Недавно я столкнулся с необходимостью организации работы службы технической помощи пользователям. Наверняка Вы использовали подобные системы – помните, когда вы получали от Вашего провайдера примерно следующее письмо: «Ваше сообщение получено нашими сотрудниками. Ему присвоен номер 123. В ближайшее время мы Вам ответим». Сообщение стоит в очередь и как только освобождается менеджер, оно отправляется ему в работу. Как сделать такую систему?
За основу, как и всегда, я беру систему управления сайтом S.Builder
Описание API интерфейса можно найти по адресу – http://api.sbuilder.ru/
Мануал по языку PHP – http://www.php.net/
Итак, поехали…
Прежде всего, хочу сказать, что для того , чтобы моя реализация задачи работала необходимо, чтобы PHP был скомпилирован с включенным модулем IMAP. Для того, чтобы проверить выводим phpinfo() и проверяем параметр Configure Command. Должны быть следующие строки: '--with-imap-ssl' '--with-imap=/usr/lib/dovecot/imap'. Скрипт будет работать в кроне.
Этот скрипт будет являться частью моей CRM – системы. Поэтому положим его в директорию cms/plugins/pl_crm/prog/. Назовем его post.php.
Начнем:
01 <?php
02 require_once '../../../kernel/prog/header.inc.php';
03 $from_user="7";
04 $to_user="1";
05 $server = "{127.0.0.1:143}INBOX";
06 $email = "support@***.ru";
07 $email_pass = "******";
Самая первая строка – подключение ядра системы S.Builder. Подключаем для того, чтобы использовать API. $from_user – переменная (в будущем вынесем ее в настройки), которая обозначает ID пользователя, от которого внутри системы будет приходить сообщение, $to_user – аналогично, только пользователь, принимающий сообщения.
$server, $email, $email_pass – тут я думаю все ясно. Первая переменная – сервер, к которому происходит подключение и папка с общениями, которую открываем. Вторые две – логин и пароль. Далее:
01 $mbox = imap_open($server, $email, $email_pass);
02 $auto_replay = false;
03 $inbox = imap_mailboxmsginfo($mbox);
Первая строка – соединение по IMAP с почтовым ящиком и выбор директории, указанной в настройках. Второе – системная настройка (надо ли отправлять auto-replay на сообщение). Третья строка – получение состояния почтового ящика на сервере (количества почтовых сообщений, дату и другие параметры). Далее создаем цикл:
01 for($i=1;$i<=$inbox->Nmsgs;$i++)
02 {
03 $header = imap_headerinfo($mbox, $i);
04
05 $subj = imap_utf8($header->Subject);
06 $sender = imap_utf8($header->fromaddress);
07 $date = $header->Date;
08 $charset = '';
09 $body = '';
10
11 $body=imap_fetchbody($mbox,$i,1,0);
12 $s = imap_fetchstructure($mbox, $i);
13 $q = 1;
14 if(count($s->parts) > 0)
15 foreach ($s->parts as $key => $val)
16 {
17 $r = getMsg($mbox, $i, $val, $q);
18 switch ($r['type'])
19 {
20 case 'text':
21 $body .= $r['res']."<hr />";
22 break;
23
24 case 'attach':
25 $body .= "К сообщению приложен файл: <a href='/uploads/pl_crm_mail/{$r['res']}' target='_blank'>{$r['res']}</a><hr />";
26 break;
27 }
28 $q++;
29 }
30 else
31 {
32 $r = getMsg($mbox, $i, $s, 1);
33 $body .= $r['res']."<hr />";
34 }
35
36 if(preg_match("/^\[TID#([^<]*)\]/", $subj))
37 {
38 // В заголовке есть TID, надо проверить к какому сообщению относится
39 }
40 else
41 {
42 // В заголовке нет TID - письмо новое.
43 $auto_replay = true;
44 $auto_replay_to = $header->from[0]->personal." <".$header->from[0]->mailbox."@".$header->from[0]->host.">";
45 $body .= "Replay to: ".$auto_replay_to.";<hr />";
46 sql_param_query("INSERT INTO sb_users_messages (um_to_id, um_from_id, um_time, um_title, um_message, um_look, um_parent_id) VALUES ($to_user, $from_user, UNIX_TIMESTAMP(NOW()), ?, ?, 0, 0)", $subj, $body);
47 $auto_replay_id = sql_insert_id();
48 sql_param_query("UPDATE sb_users_messages SET um_title = ? WHERE um_id = ?d", "[TID#".$auto_replay_id."] ".$subj, $auto_replay_id);
49 imap_delete($mbox, $i);
50 }
51 }
Что происходит в этом цикле? $inbox->Nmsgs обозначает количество сообщений в почтовом ящике, его мы и получаем. Далее по очереди обрабатываем каждое сообщение – в строках 03-07 мы получаем заголовки сообщения, выделяем из них тему письма, отправителя и дату отправления и кодируем все это в utf-8. Функцией $s = imap_fetchstructure($mbox, $i); мы получаем структуру сообщения (она понадобится далее – сообщение может быть с аттачментом и т.п.).
В строке 14 мы проверяем – состоит сообщение из одной части или из нескольких частей. В зависимости от этого, мы по-разному получаем его функцией getMsg(). Про нее я расскажу Вам позже. Забегая вперед , могу сказать, что функция возвращает массив, в котором содержится тип и собственно значение. Типов всего 2 – attach (наименование файла в attach`е сообщения) и text (текст письма).
Далее в строках 14-34 мы обрабатываем сообщение и его аттачмент. Если это простое сообщение – просто получаем его текст, если сложное – к тексту внизу дописываем ссылки на файлы в аттаче, которые функция getMsg() сохраняет в указанную нами директорию.
Далее у нас есть все составляющие для регистрации почтового запроса – заголовки, от кого пришло сообщение и его текст с аттачами. Остался один небольшой нюанс – выяснить, новое ли это сообщение или ответ на старое (которому уже присвоен номер запроса). Для этого мы используем регулярное выражение в строке 36.
Я не буду описывать в подробностях регистрацию отправления, опишу лишь один случай – письмо новое (необходимо присвоить ему номер, записать в базу данных, сохранить аттачменты и отправить пользователю автоматическое сообщение о том, что его запрос в работе). Для этого мы используем строки 43-49. Первым делом мы устанавливаем флаг auto_replay. Он означает, что в конце выполнения обработки сообщения необходимо отправить автоматический ответ пользователю. После этого мы получаем адрес отправившего запрос. К сожалению текущая версия модуля личных сообщений не позволяет записать его в отдельное поле – поэтому пойдем на уловку. Запишем его в конец сообщения. (строка 45). Далее идет запись в базу самого сообщения функцией sql_param_query, После того, как мы записали сообщение, у нас появляется его уникальный id. Надо добавить его в заголовок – что мы и делаем в строке 48. Сообщение обработано, строка 49 удаляет его из почтового ящика (зачем оно нам?).
Теперь разберемся с функцией getMsg() и автоматическим ответом пользователю.
01 function getMsg($stream, $msg, $s, $q)
02 {
03 switch ($s->type)
04 {
05 case 0:
06 $body = imap_fetchbody($stream, $msg, $q, 0);
07
08 foreach ($s->parameters as $key => $value)
09 {
10 if($value->attribute == 'charset')
11 $charset = $value->value;
12 }
13 //
14
15 if($charset == '')
16 {
17 $charset = mb_detect_encoding($body);
18 }
19
20 if($charset != 'utf-8')
21 $body=mb_convert_encoding($body, "UTF-8", $charset);
22
23 $res['type'] = 'text';
24 $res['res'] = $body;
25 break;
26
27 default:
28 $body = imap_fetchbody($stream, $msg, $q, 0);
29 if($s->encoding == 3)
30 {
31 $body = imap_base64($body);
32 }
33 elseif ($s->encoding == 4)
34 {
35 $body = imap_qprint($body);
36 }
37
38 $id = uniqid();$name = imap_utf8($s->parameters[0]->value);
39 file_put_contents($GLOBALS['dir']."uploads/pl_crm_mail/".$id.”-“.$name, $body);
40
41 $res['type'] = 'attach';
42 $res['res'] = $id."-".$name;
43 break;
44 }
45 return $res;
46 }
Итак, эта функция определяет, что за сообщение пришло и возвращает нам его. Если тип сообщения 0 (сообщение без аттачмента) то функция декодирует его в кодировку utf-8. Определение кодировки сообщения происходит по параметрам сообщения. Если вдруг параметров нет – используется функция mb_detect_encoding() модуля mb_string. Возвращается тип «text» и собственно текст сообщения в utf-8.
В другом случае мы считаем, что сообщение пришло с аттачем. В этом случае в зависимости от кодировки мы декодируем аттачмент, создаем уникальный id (на случай если в один момент пришло два письма с одинаковым названием файла) и пишем атачмент в файл. Возвращается тип «attach» и название файла.
Остался последний момент – отправка автоответа пользователю. В цикле будет такой код:
01 $if($auto_replay)
02 {
03 $auto_replay_message = "Здравствуйте!\n\nВаше письмо принято службой технической поддержки.\nВашему запросу присвоен уникальный идентификатор - $auto_replay_id.\nВ дальнейшем Вы можете продолжить эту тему переписки, если будете отвечать на письма, используя функцию Reply to Sender Вашей программы чтения почты, сохраняя в поле subject строку вида [TID#$auto_replay_id] без изменений.\n";
04 $auto_replay_subj = "[TID#".$auto_replay_id."] ".$subj;
05
06 $mail->setText($auto_replay_message);
07 $mail->setSubject($auto_replay_subj);
08 $to = array($auto_replay_to);
09 $mail->send($to, true);
10 }
А до цикла надо прописать:
01 $mail = new sbMail();
02 $mail->setFrom("Support <$email>");
03 $mail->setReturnPath($email);
Во втором фрагменте мы инициализируем объект SBuilder`а, который отвечает за отправку писем, и устанавливаем параметры. В первом фрагменте задаем оставшиеся фрагменты письма. Второй флаг в методе $mail->send() означает что сообщение нужно собирать каждый раз при отправке (ведь у нас меняется часть его параметров.
То, что я здесь описал – это скелет, то с чего можно начать написание серьезной системы обработки сообщений. В качестве перспектив можно сделать настройки, по которым сообщения будут распределяться:
Все настройки данного скрипта надо выносить в настройки системы и т.п. Если у Вас есть необходимость и желание – дорабатывайте! Я подскажу и помогу ;)
Если у кого-то есть вопросы по статье – пишите m@iqcompany.ru. Буду рад услышать отзывы и предложения по темам статей.
Максим Гасумянц (Компания «IQ - Разработка сайтов»)
Проведите конкурс среди участников CMS Magazine
Узнайте цены и сроки уже завтра. Это бесплатно и займет ≈5 минут.