PageRenderTime 48ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/code/web/private_php/ams/autoload/mail_handler.php

https://bitbucket.org/ryzom/ryzomcore
PHP | 483 lines | 312 code | 70 blank | 101 comment | 47 complexity | f40c33386961673313e9224c19199fab MD5 | raw file
Possible License(s): Apache-2.0, AGPL-3.0, GPL-3.0, LGPL-2.1
  1. <?php
  2. /**
  3. * Handles the mailing functionality.
  4. * This class covers the reading of the mail boxes of the support_groups, handling those emails, updating tickets accoring to the content & title of the emails,
  5. * but also the sending of emails after creating a new ticket and when someone else replies on your ticket.
  6. * @author Daan Janssens, mentored by Matthew Lagoe
  7. */
  8. class Mail_Handler{
  9. private $db; /**< db object used by various methods. */
  10. /**
  11. * Start a new child process and return the process id
  12. * this is used because imap might take some time, we dont want the cron parent process waiting on that.
  13. * @return return the child process id
  14. */
  15. private function mail_fork() {
  16. //Start a new child process and return the process id!
  17. if (function_exists('pcntl_fork')) {
  18. $pid = pcntl_fork();
  19. } else {
  20. $pid = getmypid ();
  21. }
  22. return $pid;
  23. }
  24. /**
  25. * Wrapper for sending emails, creates the content of the email
  26. * Based on the type of the ticketing mail it will create a specific email, it will use the language.ini files to load the correct language of the email for the receiver.
  27. * Also if the $TICKET_MAILING_SUPPORT is set to false or if the user's personal 'ReceiveMail' entry is set to false then no mail will be sent.
  28. * @param $receiver if integer, then it refers to the id of the user to whom we want to mail, if it's a string(email-address) then we will use that.
  29. * @param $ticketObj the ticket object itself, this is being used for including ticket related information into the email.
  30. * @param $content the content of a reply or new ticket
  31. * @param $type REPLY, NEW, WARNAUTHOR, WARNSENDER, WARNUNKNOWNSENDER
  32. * @param $sender (default = 0 (if it is not forwarded)) else use the id of the support group to which the ticket is currently forwarded, the support groups email address will be used to send the ticket.
  33. */
  34. public static function send_ticketing_mail($receiver, $ticketObj, $content, $type, $sender = 0) {
  35. global $TICKET_MAILING_SUPPORT;
  36. if($TICKET_MAILING_SUPPORT){
  37. global $MAIL_LOG_PATH;
  38. //error_log("Receiver: {$receiver}, content: {$content}, type: {$type}, SendingId: {$sender} \n", 3, $MAIL_LOG_PATH);
  39. if($sender == 0){
  40. //if it is not forwarded (==public == which returns 0) then make it NULL which is needed to be placed in the DB.
  41. $sender = NULL;
  42. }
  43. global $AMS_TRANS;
  44. if(is_numeric($receiver)){
  45. $webUser = new WebUsers($receiver);
  46. $lang = $webUser->getLanguage();
  47. }else{
  48. global $DEFAULT_LANGUAGE;
  49. $lang = $DEFAULT_LANGUAGE;
  50. }
  51. $variables = parse_ini_file( $AMS_TRANS . '/' . $lang . '.ini', true );
  52. $mailText = array();
  53. foreach ( $variables['email'] as $key => $value ){
  54. $mailText[$key] = $value;
  55. }
  56. switch($type){
  57. case "REPLY":
  58. $webUser = new WebUsers($receiver);
  59. if($webUser->getReceiveMail()){
  60. $subject = $mailText['email_subject_new_reply'] . $ticketObj->getTId() ."]";
  61. $txt = $mailText['email_body_new_reply_1']. $ticketObj->getTId() . $mailText['email_body_new_reply_2'] . $ticketObj->getTitle() .
  62. $mailText['email_body_new_reply_3'] . $content . $mailText['email_body_new_reply_4'];
  63. self::send_mail($receiver,$subject,$txt, $ticketObj->getTId(),$sender);
  64. }
  65. break;
  66. case "NEW":
  67. $webUser = new WebUsers($receiver);
  68. if($webUser->getReceiveMail()){
  69. $subject = $mailText['email_subject_new_ticket'] . $ticketObj->getTId() ."]";
  70. $txt = $mailText['email_body_new_ticket_1'] . $ticketObj->getTId() . $mailText['email_body_new_ticket_2'] . $ticketObj->getTitle() . $mailText['email_body_new_ticket_3']
  71. . $content . $mailText['email_body_new_ticket_4'];
  72. self::send_mail($receiver,$subject,$txt, $ticketObj->getTId(), $sender);
  73. }
  74. break;
  75. case "WARNAUTHOR":
  76. if(is_numeric($sender)){
  77. $sender = Ticket_User::get_email_by_user_id($sender);
  78. }
  79. $subject = $mailText['email_subject_warn_author'] . $ticketObj->getTId() ."]";
  80. $txt = $mailText['email_body_warn_author_1'] . $ticketObj->getTitle() .$mailText['email_body_warn_author_2'].$sender.$mailText['email_body_warn_author_3'].
  81. $sender. $mailText['email_body_warn_author_4'] ;
  82. self::send_mail($receiver,$subject,$txt, $ticketObj->getTId(), NULL);
  83. break;
  84. case "WARNSENDER":
  85. $subject = $mailText['email_subject_warn_sender'];
  86. $txt = $mailText['email_body_warn_sender'];
  87. self::send_mail($receiver,$subject,$txt, $ticketObj->getTId(), NULL);
  88. break;
  89. case "WARNUNKNOWNSENDER":
  90. $subject = $mailText['email_subject_warn_unknown_sender'];
  91. $txt = $mailText['email_body_warn_unknown_sender'];
  92. self::send_mail($receiver,$subject,$txt, $ticketObj->getTId(), NULL);
  93. break;
  94. }
  95. }
  96. }
  97. /**
  98. * send mail function that will add the email to the db.
  99. * this function is being used by the send_ticketing_mail() function. It adds the email as an entry to the `email` table in the database, which will be sent later on when we run the cron job.
  100. * @param $recipient if integer, then it refers to the id of the user to whom we want to mail, if it's a string(email-address) then we will use that.
  101. * @param $subject the subject of the email
  102. * @param $body the body of the email
  103. * @param $ticket_id the id of the ticket
  104. * @param $from the sending support_group's id (NULL in case the default group is sending))
  105. */
  106. public static function send_mail($recipient, $subject, $body, $ticket_id = 0, $from = NULL) {
  107. $id_user = NULL;
  108. if(is_numeric($recipient)) {
  109. $id_user = $recipient;
  110. $recipient = NULL;
  111. }
  112. $db = new DBLayer($db);
  113. $db->insert("email", array('Recipient' => $recipient, 'Subject' => $subject, 'Body' => $body, 'Status' => 'NEW', 'Attempts'=> 0, 'Sender' => $from,'UserId' => $id_user, 'MessageId' => 0, 'TicketId'=> $ticket_id));
  114. }
  115. /**
  116. * the cron funtion (workhorse of the mailing system).
  117. * The cron job will create a child process, which will first send the emails that are in the email table in the database, we use some kind of semaphore (a temp file) to make sure that
  118. * if the cron job is called multiple times, it wont email those mails multiple times. After this, we will read the mail inboxes of the support groups and the default group using IMAP
  119. * and we will add new tickets or new replies according to the incoming emails.
  120. */
  121. function cron() {
  122. global $cfg;
  123. global $MAIL_LOG_PATH;
  124. $default_groupemail = $cfg['mail']['default_groupemail'];
  125. $default_groupname = $cfg['mail']['default_groupname'];
  126. /*
  127. $inbox_host = $cfg['mail']['host'];
  128. $oms_reply_to = "Ryzom Ticketing Support <ticketing@".$inbox_host.">";*/
  129. global $MAIL_DIR;
  130. error_log("========================================================\n", 3, $MAIL_LOG_PATH);
  131. error_log("mailing cron Job started at: ". Helpers::outputTime(time(),0) . "\n", 3, $MAIL_LOG_PATH);
  132. //creates child process
  133. $pid = self::mail_fork();
  134. $pidfile = '/tmp/ams_cron_email_pid';
  135. if($pid) {
  136. // We're the parent process, do nothing!
  137. //INFO: if $pid =
  138. //-1: "Could not fork!\n";
  139. // 0: "In child!\n";
  140. //>0: "In parent!\n";
  141. } else {
  142. //deliver new mail
  143. //make db connection here because the children have to make the connection.
  144. $this->db = new DBLayer("lib");
  145. //if $pidfile doesn't exist yet, then start sending the mails that are in the db.
  146. if(!file_exists($pidfile)) {
  147. //create the file and write the child processes id in it!
  148. $pid = getmypid();
  149. $file = fopen($pidfile, 'w');
  150. fwrite($file, $pid);
  151. fclose($file);
  152. //select all new & failed emails & try to send them
  153. //$emails = db_query("select * from email where status = 'NEW' or status = 'FAILED'");
  154. $statement = $this->db->select("email",array(null), "Status = 'NEW' or Status = 'FAILED'");
  155. $emails = $statement->fetchAll();
  156. foreach($emails as $email) {
  157. $message_id = self::new_message_id($email['TicketId']);
  158. //if recipient isn't given, then use the email of the id_user instead!
  159. if(!$email['Recipient']) {
  160. $email['Recipient'] = Ticket_User::get_email_by_user_id($email['UserId']);
  161. }
  162. //create sending email adres based on the $sender id which refers to the department id
  163. if($email['Sender'] == NULL) {
  164. $from = $default_groupname ." <".$default_groupemail.">";
  165. } else {
  166. $group = Support_Group::getGroup($email['Sender']);
  167. $from = $group->getName()." <".$group->getGroupEmail().">";
  168. }
  169. $headers = "From: $from\r\n" . "Message-ID: " . $message_id ;
  170. if(mail($email['Recipient'], $email['Subject'], $email['Body'], $headers)) {
  171. $status = "DELIVERED";
  172. error_log("Emailed {$email['Recipient']}\n", 3, $MAIL_LOG_PATH);
  173. } else {
  174. $status = "FAILED";
  175. error_log("Email to {$email['Recipient']} failed\n", 3, $MAIL_LOG_PATH);
  176. }
  177. //change the status of the emails.
  178. $this->db->execute('update email set Status = ?, MessageId = ?, Attempts = Attempts + 1 where MailId = ?', array($status, $message_id, $email['MailId']));
  179. }
  180. unlink($pidfile);
  181. }
  182. // Check mail
  183. $sGroups = Support_Group::getGroups();
  184. //decrypt passwords in the db!
  185. $crypter = new MyCrypt($cfg['crypt']);
  186. foreach($sGroups as $group){
  187. $group->setIMAP_Password($crypter->decrypt($group->getIMAP_Password()));
  188. }
  189. $defaultGroup = new Support_Group();
  190. $defaultGroup->setSGroupId(0);
  191. $defaultGroup->setGroupEmail($default_groupemail);
  192. $defaultGroup->setIMAP_MailServer($cfg['mail']['default_mailserver']);
  193. $defaultGroup->setIMAP_Username($cfg['mail']['default_username']);
  194. $defaultGroup->setIMAP_Password($cfg['mail']['default_password']);
  195. //add default group to the list
  196. $sGroups[] = $defaultGroup;
  197. foreach($sGroups as $group){
  198. //check if group has mailing stuff filled in!
  199. if($group->getGroupEmail() != "" && $group->getIMAP_MailServer() != "" && $group->getIMAP_Username() != "" && $group->getIMAP_Password() != ""){
  200. $mbox = imap_open($group->getIMAP_MailServer(), $group->getIMAP_Username(), $group->getIMAP_Password()) or die('Cannot connect to mail server: ' . imap_last_error());
  201. $message_count = imap_num_msg($mbox);
  202. for ($i = 1; $i <= $message_count; ++$i) {
  203. //return task ID
  204. $tkey = self::incoming_mail_handler($mbox, $i,$group);
  205. if($tkey) {
  206. //base file on Ticket + timestamp
  207. $file = fopen($MAIL_DIR."/ticket".$tkey, 'w');
  208. error_log("Email was written to ".$MAIL_DIR."/ticket".$tkey."\n", 3, $MAIL_LOG_PATH);
  209. fwrite($file, imap_fetchheader($mbox, $i) . imap_body($mbox, $i));
  210. fclose($file);
  211. //mark message $i of $mbox for deletion!
  212. imap_delete($mbox, $i);
  213. }
  214. }
  215. //delete marked messages
  216. imap_expunge($mbox);
  217. imap_close($mbox);
  218. }
  219. }
  220. error_log("Child Cron job finished at ". Helpers::outputTime(time(),0) . "\n", 3, $MAIL_LOG_PATH);
  221. error_log("========================================================\n", 3, $MAIL_LOG_PATH);
  222. }
  223. }
  224. /**
  225. * creates a new message id for a email about to send.
  226. * @param $ticketId the ticket id of the ticket that is mentioned in the email.
  227. * @return returns a string, that consist out of some variable parts, a consistent part and the ticket_id. The ticket_id will be used lateron, if someone replies on the message,
  228. * to see to which ticket the reply should be added.
  229. */
  230. function new_message_id($ticketId) {
  231. $time = time();
  232. $pid = getmypid();
  233. global $cfg;
  234. global $ams_mail_count;
  235. $ams_mail_count = ($ams_mail_count == '') ? 1 : $ams_mail_count + 1;
  236. return "<ams.message".".".$ticketId.".".$pid.$ams_mail_count.".".$time."@".$cfg['mail']['host'].">";
  237. }
  238. /**
  239. * try to fetch the ticket_id out of the subject.
  240. * The subject should have a substring of the form [Ticket \#ticket_id], where ticket_id should be the integer ID of the ticket.
  241. * @param $subject the subject of an incomming email.
  242. * @return if the ticket's id is succesfully parsed, it will return the ticket_id, else it returns 0.
  243. */
  244. function get_ticket_id_from_subject($subject){
  245. $startpos = strpos($subject, "[Ticket #");
  246. if($startpos){
  247. $tempString = substr($subject, $startpos+9);
  248. $endpos = strpos($tempString, "]");
  249. if($endpos){
  250. $ticket_id = substr($tempString, 0, $endpos);
  251. }else{
  252. $ticket_id = 0;
  253. }
  254. }else{
  255. $ticket_id = 0;
  256. }
  257. return $ticket_id;
  258. }
  259. /**
  260. * Handles an incomming email
  261. * Read the content of one email by using imap's functionality. If a ticket id is found inside the message_id or else in the subject line, then a reply will be added
  262. * (if the email is not being sent from the authors email address it won't be added though and a warning will be sent to both parties). If no ticket id is found, then a new
  263. * ticket will be created.
  264. * @param $mbox a mailbox object
  265. * @param $i the email's id in the mailbox (integer)
  266. * @param $group the group object that owns the inbox.
  267. * @return a string based on the found ticket i and timestamp (will be used to store a copy of the email locally)
  268. */
  269. function incoming_mail_handler($mbox,$i,$group){
  270. global $MAIL_LOG_PATH;
  271. $header = imap_header($mbox, $i);
  272. $subject = self::decode_utf8($header->subject);
  273. $entire_email = imap_fetchheader($mbox, $i) . imap_body($mbox, $i);
  274. $subject = self::decode_utf8($header->subject);
  275. $to = $header->to[0]->mailbox;
  276. $from = $header->from[0]->mailbox . '@' . $header->from[0]->host;
  277. $fromEmail = $header->from[0]->mailbox . '@' . $header->from[0]->host;
  278. $txt = self::get_part($mbox, $i, "TEXT/PLAIN");
  279. //$html = self::get_part($mbox, $i, "TEXT/HTML");
  280. //get the id out of the email address of the person sending the email.
  281. if($from !== NULL && !is_numeric($from)){
  282. $from = Ticket_User::get_id_from_email($from);
  283. }
  284. //get ticket_id out of the message-id or else out of the subject line
  285. $ticket_id = 0;
  286. if(isset($header->references)){
  287. $pieces = explode(".", $header->references);
  288. if($pieces[0] == "<ams"){
  289. $ticket_id = $pieces[2];
  290. }else{
  291. $ticket_id = self::get_ticket_id_from_subject($subject);
  292. }
  293. }else{
  294. $ticket_id = self::get_ticket_id_from_subject($subject);
  295. }
  296. //if ticket id is found, that means it is a reply on an existing ticket
  297. if($ticket_id && is_numeric($ticket_id) && $ticket_id > 0){
  298. $ticket = new Ticket();
  299. $ticket->load_With_TId($ticket_id);
  300. //if email is sent from an existing email address in the db (else it will give an error while loading the user object)
  301. if($from != "FALSE"){
  302. $user = new Ticket_User();
  303. $user->load_With_TUserId($from);
  304. //if user has access to it!
  305. if((Ticket_User::isMod($user) or ($ticket->getAuthor() == $user->getTUserId())) and $txt != ""){
  306. Ticket::createReply($txt, $user->getTUserId(), $ticket->getTId(), 0);
  307. error_log("Email found that is a reply to a ticket at:".$group->getGroupEmail()."\n", 3, $MAIL_LOG_PATH);
  308. }else{
  309. //if user has no access to it
  310. //Warn real ticket owner + person that send the mail
  311. Mail_Handler::send_ticketing_mail($ticket->getAuthor(),$ticket, NULL , "WARNAUTHOR" , $from);
  312. Mail_Handler::send_ticketing_mail($from ,$ticket, NULL , "WARNSENDER" , NULL);
  313. error_log("Email found that was a reply to a ticket, though send by another user to ".$group->getGroupEmail()."\n", 3, $MAIL_LOG_PATH);
  314. }
  315. }else{
  316. //if a reply to a ticket is being sent by a non-user!
  317. //Warn real ticket owner + person that send the mail
  318. Mail_Handler::send_ticketing_mail($ticket->getAuthor() ,$ticket, NULL , "WARNAUTHOR" , $fromEmail);
  319. Mail_Handler::send_ticketing_mail($fromEmail ,$ticket, NULL , "WARNUNKNOWNSENDER" , NULL);
  320. error_log("Email found that was a reply to a ticket, though send by an unknown email address to ".$group->getGroupEmail()."\n", 3, $MAIL_LOG_PATH);
  321. }
  322. return $ticket_id .".".time();
  323. }else if($from != "FALSE"){
  324. //if ticket_id isn't found, create a new ticket!
  325. //if an existing email address mailed the ticket
  326. //if not default group, then forward it by giving the $group->getSGroupId's param
  327. $newTicketId = Ticket::create_Ticket($subject, $txt,1, $from, $from, $group->getSGroupId());
  328. error_log("Email regarding new ticket found at:".$group->getGroupEmail()."\n", 3, $MAIL_LOG_PATH);
  329. return $newTicketId .".".time();
  330. }else{
  331. //if it's a email that has nothing to do with ticketing, return 0;
  332. error_log("Email found that isn't a reply or new ticket, at:".$group->getGroupEmail()."\n", 3, $MAIL_LOG_PATH);
  333. return 0;
  334. }
  335. }
  336. /**
  337. * decode utf8
  338. * @param $str str to be decoded
  339. * @return decoded string
  340. */
  341. function decode_utf8($str) {
  342. preg_match_all("/=\?UTF-8\?B\?([^\?]+)\?=/i",$str, $arr);
  343. for ($i=0;$i<count($arr[1]);$i++){
  344. $str=ereg_replace(ereg_replace("\?","\?",
  345. $arr[0][$i]),base64_decode($arr[1][$i]),$str);
  346. }
  347. return $str;
  348. }
  349. /**
  350. * returns the mime type of a structure of a email
  351. * @param &$structure the structure of an email message.
  352. * @return "TEXT", "MULTIPART","MESSAGE", "APPLICATION", "AUDIO","IMAGE", "VIDEO", "OTHER","TEXT/PLAIN"
  353. * @todo take care of the HTML part of incoming emails.
  354. */
  355. function get_mime_type(&$structure) {
  356. $primary_mime_type = array("TEXT", "MULTIPART","MESSAGE", "APPLICATION", "AUDIO","IMAGE", "VIDEO", "OTHER");
  357. if($structure->subtype) {
  358. return $primary_mime_type[(int) $structure->type] . '/' .$structure->subtype;
  359. }
  360. return "TEXT/PLAIN";
  361. }
  362. //to document..
  363. function get_part($stream, $msg_number, $mime_type, $structure = false, $part_number = false) {
  364. if(!$structure) {
  365. $structure = imap_fetchstructure($stream, $msg_number);
  366. }
  367. if($structure) {
  368. if($mime_type == self::get_mime_type($structure)) {
  369. if(!$part_number) {
  370. $part_number = "1";
  371. }
  372. $text = imap_fetchbody($stream, $msg_number, $part_number);
  373. if($structure->encoding == 3) {
  374. return imap_base64($text);
  375. } else if($structure->encoding == 4) {
  376. return imap_qprint($text);
  377. } else {
  378. return $text;
  379. }
  380. }
  381. if($structure->type == 1) /* multipart */ {
  382. while(list($index, $sub_structure) = each($structure->parts)) {
  383. if($part_number) {
  384. $prefix = $part_number . '.';
  385. } else {
  386. $prefix = '';
  387. }
  388. $data = self::get_part($stream, $msg_number, $mime_type, $sub_structure,$prefix . ($index + 1));
  389. if($data) {
  390. return $data;
  391. }
  392. } // END OF WHILE
  393. } // END OF MULTIPART
  394. } // END OF STRUTURE
  395. return false;
  396. } // END OF FUNCTION
  397. }