Mailinglist
 All Data Structures Files Functions Variables Pages
MailinglistPhpImapRetrieve.class.php
Go to the documentation of this file.
1 <?php
20  function edit_form(&$form, &$form_state) {
21  parent::edit_form($form, $form_state);
22  global $cookie_domain;
23 
24  $form['connection']['settings']['box_type'] = array(
25  '#type' => 'select',
26  '#title' => t('Protocol'),
27  '#options' => array('imap' => 'IMAP', 'pop3' => 'POP3', 'local' => 'Local mbox file'),
28  '#default_value' => $this->get_setting('box_type', 'imap'),
29  '#description' => t('You can use the IMAP/POP3 protocols, or retrieve from an mbox file on the local file system.'),
30  '#required' => TRUE,
31  );
32  $ajax_settings = array(
33  'callback' => '_mailinglist_mailbox_test',
34  'wrapper' => 'mailinglist_test_results',
35  'event' => 'change',
36  'progress' => array(
37  'type' => 'throbber',
38  'message' => t('Please wait - testing connection settings...'),
39  ),
40  );
41  $form['connection']['settings']['folder'] = array(
42  '#type' => 'textfield',
43  '#title' => t('Folder'),
44  '#default_value' => $this->get_setting('folder'),
45  '#description' => t('The folder where the mail is stored. If you want this mailbox to read from a local mbox file, give the path relative to the Drupal installation directory.'),
46  '#ajax' => $ajax_settings,
47  '#required' => TRUE,
48  );
49  $form['connection']['settings']['domain'] = array(
50  '#type' => 'textfield',
51  '#title' => t('Domain'),
52  '#default_value' => $this->get_setting('domain'),
53  '#description' => t('The domain of the server used to collect mail.'),
54  '#ajax' => $ajax_settings,
55  '#required' => TRUE,
56  );
57  $form['connection']['settings']['port'] = array(
58  '#type' => 'textfield',
59  '#title' => t('Port'),
60  '#size' => 5,
61  '#maxlength' => 5,
62  '#default_value' => $this->get_setting('port'),
63  '#description' => t('The mailbox port number (usually 110 for POP3, 143 for IMAP).'),
64  '#element_validate' => array('element_validate_integer_positive'),
65  '#ajax' => $ajax_settings,
66  '#required' => TRUE,
67  );
68  $form['connection']['settings']['name'] = array(
69  '#type' => 'textfield',
70  '#title' => t('Username'),
71  '#default_value' => $this->get_setting('name'),
72  '#description' => t('This username is used while logging into this mailbox during mail retrieval.'),
73  '#ajax' => $ajax_settings,
74  );
75  $form['connection']['settings']['pass'] = array(
76  '#type' => 'textfield',
77  '#title' => t('Password'),
78  '#default_value' => _mailinglist_decode_password($this->get_setting('pass')),
79  '#description' => t('The mailbox password corresponding to the username above. Consider using a non-vital password, since this field is stored with minimal encryption in the database and displayed here.'),
80  '#ajax' => $ajax_settings,
81  );
82  // Allow administrators to configure the mailbox with extra IMAP commands (notls, novalidate-cert etc.)
83  $form['connection']['settings']['extraimap'] = array(
84  '#type' => 'textfield',
85  '#title' => t('Extra commands'),
86  '#default_value' => $this->get_setting('extraimap'),
87  '#description' => t('In some circumstances you need to issue extra commands to connect to your mail server (for example "/notls", "/novalidate-cert" etc.). See documentation for <a href="@imap-open">imap_open</a>.', array('@imap-open' => url('http://php.net/imap_open'))),
88  '#ajax' => $ajax_settings,
89  );
90  $form['connection']['settings']['results'] = array(
91  '#type' => 'container',
92  '#attributes' => array(
93  'id' => 'mailinglist_test_results',
94  ),
95  );
96 
97  $form['extra']['settings']['flag_after_read'] = array(
98  '#type' => 'checkbox',
99  '#title' => t('Mark messages as seen/read after they are processed?'),
100  '#default_value' => $this->get_setting('flag_after_read', TRUE),
101  '#description' => t('Note that messages cannot be marked as seen/read for POP3 accounts.'),
102  );
103  $form['extra']['settings']['delete_after_read'] = array(
104  '#type' => 'checkbox',
105  '#title' => t('Delete messages after they are processed?'),
106  '#default_value' => $this->get_setting('delete_after_read', TRUE),
107  '#description' => t('Uncheck this box to leave read messages in the mailbox. They will not be processed again unless they become marked as unread. If you selected "POP3" as your mailbox type, you must check this box.'),
108  );
109  }
110 
115  public function edit_form_validate(&$form, &$form_state) {
116  parent::edit_form_validate($form, $form_state);
117 
118  // If POP3 mailbox is chosen, messages should be deleted after processing.
119  // Do not set an actual error because this is helpful for testing purposes.
120  if ($form_state['values']['settings']['box_type'] == 'pop3' && $form_state['values']['settings']['delete_after_read'] == 0) {
121  drupal_set_message(t('Unless you check off "Delete messages after they are processed" when using a POP3 mailbox, old emails will be re-imported each time the mailbox is processed. You can partially prevent this by mapping Message ID to a unique target in the processor configuration - see INSTALL.txt or advanced help for more information'), 'warning');
122  }
123  if (($form_state['values']['settings']['box_type'] == 'pop3' && $form_state['values']['settings']['port'] != 110) || ($form_state['values']['settings']['box_type'] == 'imap' && $form_state['values']['settings']['port'] != 143)) {
124  drupal_set_message(t('Non-standard Port:') . ' ' . $form_state['values']['settings']['port'] . ' (' . $form_state['values']['settings']['box_type'] . ')', 'warning');
125  }
126  }
131  public function edit_form_submit(&$form, &$form_state) {
132  parent::edit_form_submit($form, $form_state);
133  }
134 
136 
141  /*
142  * Connect to mailbox and run message retrieval.
143  *
144  * @return array
145  * Retrieved messages.
146  */
147  public function get_message_list($max = 0) {
148  dpm($this->settings);
149  extract($this->settings);
150  $new = array();
151  /*
152  if ($result = $this->open_mailbox()) {
153  $new = $this->get_unread_messages($result);
154  }
155  else {
156  imap_errors();
157  throw new Exception(t('Unable to connect to %mail. Please check the <a href="@mailbox-edit">connection settings</a> for this mailbox.', array('%mail' => $this->mail, '@mailbox-edit' => url('admin/structure/mailinglist/list/' . $this->mail . '/edit'))));
158  }
159  */
160  return $new;
161  }
163 
164 
165 /*************************************************************/
166  function retrieve() {
167  extract($this->settings);
168  $result = $this->open_mailbox();
169  if ($result) {
170  $new = $this->get_unread_messages($result);
171  }
172  else {
173  imap_errors();
174  throw new Exception(t('Unable to connect to %mail. Please check the <a href="@mailbox-edit">connection settings</a> for this mailbox.', array('%mail' => $this->mail, '@mailbox-edit' => url('admin/structure/mailinglist/list/' . $this->mail . '/edit'))));
175  }
176  $messages = array();
177  $retrieved = 0;
178  while ($new && (!$limit || $retrieved < $limit)) {
179  $message = $this->retrieve_message($result, array_shift($new));
180  if ($message) {
181  $messages[] = $message;
182  }
183  ++$retrieved;
184  }
185  watchdog('mailinglist', 'Mailbox %mail was checked and contained %retrieved messages.', array('%mail' => $this->admin_title, '%retrieved' => $retrieved), WATCHDOG_INFO);
186  $this->close_mailbox($result);
187  return $messages;
188  }
189 
196  function test() {
197  extract($this->settings);
198  $ret = array();
199 
200  $is_local = ($type == 'local');
201  $folder_is_set = (!empty($folder) && $folder != 'INBOX');
202  $connect_is_set = !empty($domain) && !empty($port) && !empty($name) && !empty($pass);
203 
204  if (($is_local && $folder_is_set) || (!$is_local && $connect_is_set)) {
205  $result = $this->open_mailbox();
206  if ($result) {
207  $ret[] = array('severity' => 'status', 'message' => t('Mailinglist was able to connect to the mailbox.'));
208  $box = $this->mailbox_string();
209  $status = imap_status($result, $box, SA_MESSAGES);
210  if ($status) {
211  $ret[] = array('severity' => 'status', 'message' => t('There are @messages messages in the mailbox folder.', array('@messages' => $status->messages)));
212  }
213  else {
214  $ret[] = array('severity' => 'warning', 'message' => t('Mailinglist could not open the specified folder'));
215  }
216  $this->close_mailbox($result);
217  }
218  else {
219  imap_errors();
220  $ret[] = array('severity' => 'error', 'message' => t('Mailinglist could not access the mailbox using these settings'));
221  }
222  }
223  return $ret;
224  }
228  function purge_message($message) {
229  $this->log("Purge Message: " . $message);
230  if (isset($message['imap_uid'])) {
231  $result = $this->open_mailbox();
232  if ($result) {
233  if ($this->settings['delete_after_read']) {
234  imap_delete($result, $message['imap_uid'], FT_UID);
235  }
236  elseif (!isset($this->settings['flag_after_read']) || ($this->settings['flag_after_read'])) {
237  imap_setflag_full($result, (string)$message['imap_uid'], '\Seen', FT_UID);
238  }
239  $this->close_mailbox($result);
240  }
241  else {
242  drupal_set_message(t('Unable to connect to mailbox.'));
243  watchdog('mailinglist', 'Unable to connect to %mail', array('%mail' => $this->mail), WATCHDOG_ERROR);
244  }
245  }
246  }
247 
254  function open_mailbox() {
255  extract($this->settings);
256 
257  if (!function_exists('imap_open')) {
258  throw new Exception(t('The PHP IMAP extension must be enabled in order to use Mailinglist.'));
259  }
260  $box = $this->mailbox_string();
261  if ($type != 'local') {
262  $result = imap_open($box, $name, $pass, NULL, 1);
263  }
264  else {
265  $orig_home = getenv('HOME');
266  // This is hackish, but better than using $_SERVER['DOCUMENT_ROOT']
267  $new_home = realpath(drupal_get_path('module', 'node') . '/../../');
268  if (!putenv("HOME=$new_home")) {
269  throw new Exception(t('Could not set home directory to %home.', array('%home' => $new_home)));
270  }
271  $result = imap_open($box, '', '', NULL, 1);
272  putenv("HOME=$orig_home");
273  }
274  return $result;
275  }
276 
280  function mailbox_string() {
281  extract($this->settings);
282 
283  switch ($type) {
284  case 'imap':
285  return '{' . $domain . ':' . $port . $extraimap . '}' . $folder;
286  case 'pop3':
287  return '{' . $domain . ':' . $port . '/pop3' . $extraimap . '}' . $folder;
288  case 'local':
289  return $folder;
290  }
291  }
292 
300  function get_part($stream, $msg_number, $mime_type, $structure = FALSE, $part_number = FALSE, $encoding) {
301  if (!$structure) {
302  $structure = imap_fetchstructure($stream, $msg_number);
303  }
304  if ($structure) {
305  foreach ($structure->parameters as $parameter) {
306  if (drupal_strtoupper($parameter->attribute) == 'CHARSET') {
307  $encoding = $parameter->value;
308  }
309  }
310  if ($mime_type == $this->get_mime_type($structure)) {
311  if (!$part_number) {
312  $part_number = '1';
313  }
314  $text = imap_fetchbody($stream, $msg_number, $part_number, FT_PEEK);
315  if ($structure->encoding == ENCBASE64) {
316  return drupal_convert_to_utf8(imap_base64($text), $encoding);
317  }
318  elseif ($structure->encoding == ENCQUOTEDPRINTABLE) {
319  return drupal_convert_to_utf8(quoted_printable_decode($text), $encoding);
320  }
321  else {
322  return drupal_convert_to_utf8($text, $encoding);
323  }
324  }
325  if ($structure->type == TYPEMULTIPART) { /* multipart */
326  $prefix = '';
327  while (list($index, $sub_structure) = each($structure->parts)) {
328  if ($part_number) {
329  $prefix = $part_number . '.';
330  }
331  $data = $this->get_part($stream, $msg_number, $mime_type, $sub_structure, $prefix . ($index + 1), $encoding);
332  if ($data) {
333  return $data;
334  }
335  }
336  }
337  }
338  return FALSE;
339  }
340 
359  function get_parts($stream, $msg_number, $max_depth = 10, $depth = 0, $structure = FALSE, $part_number = 1) {
360  $parts = array();
361 
362  // Load Structure.
363  if (!$structure) {
364  $structure = imap_fetchstructure($stream, $msg_number);
365  if (!$structure)
366  watchdog('mailinglist', 'Could not fetch structure for message number %msg_number', array('%msg_number' => $msg_number), WATCHDOG_NOTICE);
367  return $parts;
368  }
369  // Recurse into multipart messages.
370  if ($structure->type == TYPEMULTIPART) {
371  // Restrict recursion depth.
372  if ($depth >= $max_depth) {
373  watchdog('mailinglist', 'Maximum recursion depths met in mailhander_get_structure_part for message number %msg_number.', array('%msg_number' => $msg_number), WATCHDOG_NOTICE);
374  return $parts;
375  }
376  $prefix = '';
377  foreach ($structure->parts as $index => $sub_structure) {
378  // If a part number was passed in and we are a multitype message, prefix the
379  // the part number for the recursive call to match the imap4 dot seperated part indexing.
380  if ($part_number) {
381  $prefix = $part_number . '.';
382  }
383  $sub_parts = $this->get_parts($stream, $msg_number, $max_depth, $depth + 1,
384  $sub_structure, $prefix . ($index + 1));
385  $parts = array_merge($parts, $sub_parts);
386  }
387  return $parts;
388  }
389 
390  // Per Part Parsing.
391 
392  // Initalize file object like part structure.
393  $part = new stdClass();
394  $part->attributes = array();
395  $part->filename = 'unnamed_attachment';
396  if (!$part->filemime = $this->get_mime_type($structure)) {
397  watchdog('mailinglist', 'Could not fetch mime type for message part. Defaulting to application/octet-stream.', array(), WATCHDOG_NOTICE);
398  $part->filemime = 'application/octet-stream';
399  }
400 
401  if ($structure->ifparameters) {
402  foreach ($structure->parameters as $parameter) {
403  switch (drupal_strtoupper($parameter->attribute)) {
404  case 'NAME':
405  case 'FILENAME':
406  $part->filename = $parameter->value;
407  break;
408  default:
409  // put every thing else in the attributes array;
410  $part->attributes[$parameter->attribute] = $parameter->value;
411  }
412  }
413  }
414 
415  // Handle Content-Disposition parameters for non-text types.
416  if ($structure->type != TYPETEXT && $structure->ifdparameters) {
417  foreach ($structure->dparameters as $parameter) {
418  switch (drupal_strtoupper($parameter->attribute)) {
419  case 'NAME':
420  case 'FILENAME':
421  $part->filename = $parameter->value;
422  break;
423  // put every thing else in the attributes array;
424  default:
425  $part->attributes[$parameter->attribute] = $parameter->value;
426  }
427  }
428  }
429 
430  // Store part id for reference.
431  if (!empty($structure->id)) {
432  $part->id = $structure->id;
433  }
434 
435  // Retrieve part and convert MIME encoding to UTF-8
436  if (!$part->data = imap_fetchbody($stream, $msg_number, $part_number, FT_PEEK)) {
437  watchdog('mailinglist', 'No Data!!', array(), WATCHDOG_ERROR);
438  return $parts;
439  }
440 
441  // Decode as necessary.
442  if ($structure->encoding == ENCBASE64) {
443  $part->data = imap_base64($part->data);
444  }
445  elseif ($structure->encoding == ENCQUOTEDPRINTABLE) {
446  $part->data = quoted_printable_decode($part->data);
447  }
448  // Convert text attachment to UTF-8.
449  elseif ($structure->type == TYPETEXT) {
450  $part->data = imap_utf8($part->data);
451  }
452 
453  // Always return an array to satisfy array_merge in recursion catch, and
454  // array return value.
455  $parts[] = $part;
456  return $parts;
457  }
458 
462  function get_mime_type(&$structure) {
463  static $primary_mime_type = array('text', 'multipart', 'message', 'application', 'audio', 'image', 'video', 'other');
464  $type_id = (int) $structure->type;
465  if (isset($primary_mime_type[$type_id]) && !empty($structure->subtype)) {
466  return $primary_mime_type[$type_id] . '/' . drupal_strtolower($structure->subtype);
467  }
468  return 'text/plain';
469  }
470 
479  function get_unread_messages($result) {
480  $unread_messages = array();
481  $number_of_messages = imap_num_msg($result);
482  for ($i = 1; $i <= $number_of_messages; $i++) {
483  $header = imap_header($result, $i);
484  if ($header->Unseen == 'U' || $header->Recent == 'N') {
485  $unread_messages[] = $i;
486  }
487  }
488  return $unread_messages;
489  }
490 
501  function retrieve_message($result, $msg_number) {
502  extract($this->settings);
503  dpm("MailinglistPhpImapRetreive::retreive_message");
504  $raw_headers = imap_fetchheader($result, $msg_number, FT_PREFETCHTEXT);
506  $header = imap_headerinfo($result, $msg_number);
507 
508  // Initialize the subject in case it's missing.
509  if (!isset($header->subject)) {
510  $header->subject = '';
511  }
512  // Parse MIME parts, so all mailinglist modules have access to
513  // the full array of mime parts without having to process the email.
514  $mimeparts = $this->get_parts($result, $msg_number);
515 
516  $body_text = $this->get_part($result, $msg_number, 'text/plain', FALSE, FALSE, $encoding);
517  $body_html = $this->get_part($result, $msg_number, 'text/html', FALSE, FALSE, $encoding);
518  if (!$body_text && $body_html) {
519  $body_text = $body_html;
520  }
521  elseif ($body_text && !$body_html) {
522  $body_html = $body_text;
523  }
524  // Is this an empty message with no body and no mimeparts?
525  if (!$body_text && !$body_html && !$mimeparts) {
526  $message = FALSE;
527  }
528  else {
529  $imap_uid = ($type == 'pop3') ? $this->fetch_uid($msg_number) : imap_uid($result, $msg_number);
530  $message = compact('header', 'raw_headers', 'body_text', 'body_html', 'mimeparts', 'imap_uid');
531  }
532  dpm($message);
533  return $message;
534  }
535 
539  function close_mailbox($result) {
540  imap_errors();
541  imap_close($result, CL_EXPUNGE);
542  }
543 
549  function fetch_uid($msg_number) {
550  extract($this->settings);
551  $retval = 0;
552  $fp = fsockopen($domain, $port);
553  if ($fp > 0) {
554  $buf = fgets($fp, 1024);
555  fwrite($fp, "USER $name\r\n");
556  $buf = fgets($fp, 1024);
557  fwrite($fp, "PASS $pass\r\n");
558  $buf = fgets($fp, 1024);
559  fwrite($fp, "UIDL $msg_number\r\n");
560  $retval=fgets($fp, 1024);
561  fwrite($fp, "QUIT\r\n");
562  $buf = fgets($fp, 1024);
563  fclose($fp);
564  }
565  return drupal_substr($retval, 6, 30);
566  }
567 }
Retrieve messages using PHP IMAP library.
fetch_uid($msg_number)
Fetch UID for a message in a POP mailbox.
purge_message($message)
mark message for deletion
get_setting($name, $def=NULL)
get_settings()
edit_form_validate(&$form, &$form_state)
Implements ctools_export_ui::edit_form_validate().
get_unread_messages($result)
Obtain the number of unread messages for an imap stream.
log($str, $level=self::LOG_NOTICE, $parms=array())
log()
retrieve_message($result, $msg_number)
Retrieve individual messages from an IMAP result.
edit_form_submit(&$form, &$form_state)
Implements ctools_export_ui::edit_form_submit().
get_parts($stream, $msg_number, $max_depth=10, $depth=0, $structure=FALSE, $part_number=1)
Returns an array of parts as file objects.
_mailinglist_decode_password($code)
_mailinglist_decode_password
mailbox_string()
Constructs a mailbox string based on mailbox object.
get_message_list($max=0)
Returns a list of messages that are available.
get_mime_type(&$structure)
Retrieve MIME type of the message structure.
test()
Test connection to a mailbox.
edit_form(&$form, &$form_state)
Implements ctools_export_ui::edit_form().
get_part($stream, $msg_number, $mime_type, $structure=FALSE, $part_number=FALSE, $encoding)
Returns the first part with the specified mime_type.
Retrieve messages from a Mailinglist Mailbox.
open_mailbox()
Establish IMAP stream connection to specified mailbox.