freemail_email_processor.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. <?php
  2. /*
  3. This is a base class for email processors.
  4. It needs to be extended by a specific email processor, stored in the email_processors directory.
  5. */
  6. class sloodle_freemail_email_processor {
  7. // The following hold the raw data from the email.
  8. var $_subject;
  9. var $_from_address;
  10. var $_html_body;
  11. var $_plain_body;
  12. var $_charset;
  13. var $_attachments = array();
  14. // The following are filled during parsing.
  15. var $_images= array();
  16. var $_userid = null;
  17. var $_prepared_body;
  18. var $_prepared_subject;
  19. var $_importer;
  20. static function available_email_processors() {
  21. $processor_dir = dirname(__FILE__).'/email_processors';
  22. if (!$dh = opendir($processor_dir)) {
  23. return false;
  24. }
  25. $processors = array();
  26. while (($processor_file = readdir($dh)) !== false) {
  27. if (preg_match('/^(sloodle_freemail_\w+_email_processor).php$/', $processor_file, $matches)) {
  28. $clsname = $matches[1];
  29. require_once($processor_dir.'/'.$processor_file);
  30. if (!class_exists($clsname)) {
  31. continue;
  32. }
  33. if (!$clsname::is_available()) {
  34. continue;
  35. }
  36. $processors[] = new $clsname;
  37. }
  38. }
  39. return $processors;
  40. }
  41. // Adds an attachment.
  42. // Multi-part emails can have lots of random little attachments.
  43. // In sloodle we are interested in images
  44. // ...and narrow down to provide them in get_images().
  45. function add_attachment($attachment_name, $attachment_data) {
  46. $this->_attachments[$attachment_name] = $attachment_data;
  47. }
  48. function add_image($filename, $data) {
  49. $this->_images[$filename] = $data;
  50. }
  51. // Return true to say that this processor can process the email.
  52. // This will include finding an importer that can handle the email.
  53. function is_email_processable() {
  54. return false;
  55. }
  56. // Import the message or return false on failure
  57. function import() {
  58. if (!$this->_importer) {
  59. return false;
  60. }
  61. $this->_importer->set_user_id($this->_userid);
  62. $this->_importer->set_title($this->_prepared_subject);
  63. $this->_importer->set_body($this->_prepared_body);
  64. foreach($this->_images as $n => $imgdata) {
  65. $this->_importer->add_image($n, $imgdata);
  66. }
  67. return $this->_importer->import();
  68. }
  69. // Transform the raw text from the email into whatever we want to put into Moodle.
  70. // You probably want to overload this.
  71. // In the sloodle case this will consist of stripping Second Life advertising
  72. // ...and adding a URL pointing to the user's location.
  73. function prepare() {
  74. $this->_prepared_body = $this->_plain_body;
  75. $this->_prepared_subject = $this->_subject;
  76. if (!$this->_userid = $this->user_id_for_email($this->_from_address)) {
  77. return false;
  78. }
  79. return true;
  80. }
  81. // Return the user ID
  82. function get_user_id() {
  83. return $this->_userid;
  84. }
  85. function set_charset($c) {
  86. $this->_charset = $c;
  87. }
  88. function get_charset() {
  89. return $this->_charset;
  90. }
  91. function get_plain_body() {
  92. return $this->_plain_body;
  93. }
  94. function set_plain_body($b) {
  95. $this->_plain_body = $b;
  96. }
  97. // Return the message body
  98. function get_html_body() {
  99. return $this->_html_body;
  100. }
  101. function set_html_body($m) {
  102. $this->_html_body = $m;
  103. }
  104. function set_subject($s) {
  105. $this->_subject = $s;
  106. }
  107. function set_from_address($e) {
  108. $this->_from_address = $e;
  109. }
  110. function get_subject() {
  111. return $this->_subject;
  112. }
  113. function load_importer() {
  114. $importers = sloodle_freemail_moodle_importer::available_moodle_importers();
  115. if (!count($importers)) {
  116. return false;
  117. }
  118. foreach($importers as $importer) {
  119. if ($importer->is_email_importable()) {
  120. $this->_importer = $importer;
  121. return true;
  122. }
  123. }
  124. return false;
  125. }
  126. function user_id_for_email($email) {
  127. if (is_null($email)) {
  128. return null;
  129. }
  130. global $DB;
  131. $this->_userid = $DB->get_field('user', 'id', array('email'=>$email));
  132. return $this->_userid;
  133. }
  134. // By default, just tell the importer to notify them.
  135. // This would normally be an email.
  136. // Some handler may want to do their own handling
  137. // ..and just get the subject and body from the importer.
  138. // For example, in SLOODLE we send an in-world instant message.
  139. function notify_user() {
  140. if (!$importer = $this->_importer) {
  141. return false;
  142. }
  143. return $importer->notify_user();
  144. }
  145. static function verbose_output($verbose, $msg) {
  146. if ($verbose) {
  147. print $msg."\n";
  148. }
  149. }
  150. static function read_mail($cfg, $verbose, $daemon, $handler = null, $nodelete = false, $cron = false) {
  151. // This allows you to hard-code some settings in your config.php and use them in preference to whatever might be set in the web UI.
  152. // This is useful to us at Avatar Classroom in a multi-site setting, but probably not to anybody else.
  153. $cfg = isset($cfg->sloodle_freemail_force_settings) ? $cfg->sloodle_freemail_force_settings : $cfg;
  154. if ($cfg->sloodle_freemail_mail_box_settings == '') {
  155. return false;
  156. }
  157. $statuses = array(
  158. 'result' => array(),
  159. 'errors' => array(),
  160. 'messages' => array(
  161. )
  162. );
  163. $giveup = false;
  164. $msgcount = 0;
  165. $email_processors = sloodle_freemail_email_processor::available_email_processors();
  166. if (!count($email_processors)) {
  167. sloodle_freemail_email_processor::verbose_output($verbose, "No email processors available, aborting.");
  168. $statuses['errors']["-1"] = "No email processors available, aborting.";
  169. $giveup = true;
  170. }
  171. // In daemon mode, the handler is kept alive between calls to this function with its connection open.
  172. sloodle_freemail_email_processor::verbose_output($verbose, "Trying to get connection...");
  173. $handler = !is_null($handler) ? $handler : new sloodle_freemail_imap_message_handler();
  174. if (!$giveup) {
  175. sloodle_freemail_email_processor::verbose_output($verbose, "Connecting...");
  176. if (!$handler->connect($cfg->sloodle_freemail_mail_box_settings, $cfg->sloodle_freemail_mail_user_name, $cfg->sloodle_freemail_mail_user_pass)) {
  177. sloodle_freemail_email_processor::verbose_output($verbose, "Connection failed.");
  178. $statuses['errors']["-2"] = "Connection failed. Could not fetch email.";
  179. $giveup = true;
  180. }
  181. }
  182. if (!$giveup) {
  183. if (!$msgcount = $handler->count()) {
  184. // In daemon mode, keep the connection open, and return the handler object so we can reuse it.
  185. if ($daemon) {
  186. return $handler;
  187. }
  188. $handler->close();
  189. sloodle_freemail_email_processor::verbose_output($verbose, "No messages found.");
  190. $statuses['result']["1"] = "No messages.";
  191. $giveup = true;
  192. }
  193. }
  194. if (!$giveup) {
  195. sloodle_freemail_email_processor::verbose_output($verbose, "Got $msgcount messages.");
  196. if ($msgcount > 0) {
  197. if ($msgcount > $cfg->sloodle_freemail_mail_maxcheck) {
  198. $msgcount = $cfg->sloodle_freemail_mail_maxcheck;
  199. }
  200. for ($mid = 1; $mid <= $msgcount; $mid++) {
  201. $statuses['messages'] = array();
  202. sloodle_freemail_email_processor::verbose_output($verbose, "Considering loading message with ID :$mid:");
  203. // Load the header first so that we can check what we need to know before downloading the rest.
  204. if (!$handler->load_header($mid)) {
  205. $statuses['messages'][] = array(
  206. 'errors' => array('-101' => 'Could not load header')
  207. );
  208. continue;
  209. }
  210. $subject = $handler->get_subject();
  211. $fromaddress = $handler->get_from_address();
  212. $toaddress = $handler->get_to_address();
  213. if (!strtolower($toaddress) == strtolower($cfg->sloodle_freemail_mail_email_address)) {
  214. print "not for us: $toaddress";
  215. // Not for us.
  216. continue;
  217. }
  218. $info = array(
  219. 'subject' => $subject,
  220. 'fromaddress' => $fromaddress
  221. );
  222. $size_in_bytes = $handler->get_size_in_bytes();
  223. if ($size_in_bytes > $cfg->sloodle_freemail_mail_maxsize) {
  224. $statuses['messages'][] = array(
  225. 'errors' => array('-101' => 'Could not load header.'),
  226. 'info' => $info
  227. );
  228. continue;
  229. }
  230. sloodle_freemail_email_processor::verbose_output($verbose, "Message size :$size_in_bytes: small enough - continuing.");
  231. // TODO: Separate load_header and load_body so we don't load the whole thing if it's too big.
  232. if (!$handler->load($mid)) {
  233. $statuses['messages'][] = array(
  234. 'errors' => array('-102' => 'Could not load.'),
  235. 'info' => $info
  236. );
  237. continue;
  238. }
  239. sloodle_freemail_email_processor::verbose_output($verbose, "Loaded message...");
  240. $htmlmsg = $handler->get_html_message();;
  241. $plainmsg = $handler->get_plain_message();;
  242. $charset = $handler->get_charset();
  243. $attachments = $handler->get_attachments();
  244. foreach($email_processors as $processor) {
  245. sloodle_freemail_email_processor::verbose_output($verbose, "Trying processor...");
  246. $processor->set_subject($subject);
  247. $processor->set_from_address($fromaddress);
  248. $processor->set_plain_body($plainmsg);
  249. $processor->set_html_body($htmlmsg);
  250. $processor->set_charset($charset);
  251. foreach($attachments as $attachment_filename => $attachment_body) {
  252. $processor->add_attachment($attachment_filename, $attachment_body);
  253. }
  254. sloodle_freemail_email_processor::verbose_output($verbose, "Preparing message...");
  255. // Couldn't make sense of it, skip
  256. if (!$processor->prepare()) {
  257. $statuses['messages'][] = array(
  258. 'errors' => array('-103' => 'Could not prepare email.') ,
  259. 'info' => $info
  260. );
  261. sloodle_freemail_email_processor::verbose_output($verbose, "Could not prepare email.");
  262. continue;
  263. }
  264. // Couldn't find anyone to process it, skip.
  265. if (!$processor->load_importer()) {
  266. sloodle_freemail_email_processor::verbose_output($verbose, "Could not load importer.");
  267. $statuses['messages'][] = array(
  268. 'errors' => array('-104' => 'Could not load importer.'),
  269. 'info' => $info
  270. );
  271. continue;
  272. }
  273. // Processor can't handle this kind of email.
  274. if (!$processor->is_email_processable()) {
  275. sloodle_freemail_email_processor::verbose_output($verbose, "Processor cannot handle this email. Will let others try.");
  276. $statuses['messages'][] = array(
  277. 'errors' => array('-104' => 'Could not load importer.'),
  278. 'info' => $info
  279. );
  280. continue;
  281. }
  282. // TODO: Get this working.
  283. // Ideally we should mark messages as flagged before we start to import them
  284. // ...and skip over messages that are already flagged.
  285. // This should prevent multiple processes running at the same time from tripping over each other and importing the same message multiple times.
  286. // Mark the message as flagged
  287. // $handler->mark_flagged($mid);
  288. sloodle_freemail_email_processor::verbose_output($verbose, "Importing...");
  289. if (!$processor->import()) {
  290. sloodle_freemail_email_processor::verbose_output($verbose, "Importing failed.");
  291. $statuses['messages'][] = array(
  292. 'errors' => array('-105' => 'Importing failed.'),
  293. 'info' => $info
  294. );
  295. continue;
  296. }
  297. sloodle_freemail_email_processor::verbose_output($verbose, "Notifying user...");
  298. if (!$processor->notify_user()) {
  299. $statuses['messages'][] = array(
  300. 'success' => array('107' => 'Imported, but could not notify user..'),
  301. 'errors' => array('-106' => 'Imported, but could not notify user..'),
  302. 'info' => $info
  303. );
  304. break;
  305. }
  306. sloodle_freemail_email_processor::verbose_output($verbose, "Handling of this email complete.");
  307. $statuses['messages'][] = array(
  308. 'success' => array('107' => 'Imported, but could not notify user..'),
  309. 'errors' => array('-106' => 'Imported, but could not notify user..'),
  310. 'info' => $info
  311. );
  312. break;
  313. }
  314. // skipping subcommand stuff
  315. // list($subcomm, $messcomm) = freemail_getcommands($msg->header[$mid]['subject'], $messagebody['message'], $commands);
  316. if ($nodelete) {
  317. sloodle_freemail_email_processor::verbose_output($verbose, "Skipping deletion of message $mid because you asked for nodelete.");
  318. } else {
  319. sloodle_freemail_email_processor::verbose_output($verbose, "Deleting message $mid.");
  320. if (!$handler->delete($mid)) {
  321. sloodle_freemail_email_processor::verbose_output($verbose, "Deletion of message $mid failed.");
  322. }
  323. }
  324. //imap_delete($mailbox, $mid);
  325. //print "skipping user mail content";
  326. }
  327. }
  328. $handler->expunge();
  329. }
  330. if ($cfg->sloodle_freemail_mail_admin_email) {
  331. // in daemon mode, only send a report if some messages were actually processed.
  332. if ( (!$daemon & !$cron) || $msgcount) {
  333. $subject = "Email processing report";
  334. $body = sloodle_freemail_email_processor::status_text($statuses);
  335. mail($cfg->sloodle_freemail_mail_admin_email, $subject, $body);
  336. }
  337. }
  338. // In daemon mode, keep the handler with its connection alive and return it so it can be used again next time.
  339. if ($daemon) {
  340. return $handler;
  341. }
  342. $handler->close();
  343. return true;
  344. }
  345. static function status_text($statuses) {
  346. $str = '';
  347. if (count($statuses['errors'])) {
  348. $str .= 'Fetching email failed:'."\n";
  349. $str .= implode("\n", $statuses['errors'])."\n";
  350. }
  351. $str .= count($statuses['messages']).' messages processed.';
  352. return $str;
  353. }
  354. }