Перекидывание почты в базу: PHP, IMAP, MySQL
Возникла необходимость перелить в базу данных содержимое почтового ящика. По получившемуся архиву должен осуществляться относительно быстрый поиск. Следующий скрипт забирает письма по IMAP, парсит их, помещает в MySQL, и при необходимости удаляет обработанную почту (отвечающий за удаление кусок кода нужно раскомментировать). Если удалять письма не нужно, то для более быстрой работы можно использовать опцию readonly при инициализации соединения с почтовым сервером.
В скрипте не предусмотрена обработка писем во вложенных папках, так как это не требовалось. Данные собираются в поля: дата, отправитель, получатели (to, cc и bcc скомбинированы в одну строку) и тема письма. Полная копия письма также сохраняется в BLOB.
Код писался на скорую руку и при определённых условиях фэйлит, но задачу свою он в итоге с нескольких попыток всё же выполнил. Так что отсюда можно выдрать несколько полезных моментов, в частности, обработку темы письма.
В случае остановки скрипта на произвольном месте, можно посмотреть в логе предыдущий обработанный UID письма и передать его в качестве параметра при следующем запуске, при этом обработка почты продолжится с указанного места.
#!/usr/bin/php -f
< ?php
echo "Running...\n";
/* login setup */
// $imap_hostname = '{mail.example.com:993/imap/ssl/readonly}INBOX';
// $imap_username = 'user@example.com';
// $imap_password = 'password';
// $imap_hostname = '{mail.example.com:143/debug/imap2/notls/readonly}INBOX';
// $imap_hostname = '{mail.example.com:993/debug/imap/tls/novalidate-cert/readonly}INBOX';
$imap_hostname = '{mail.example.com:143/imap4/notls}INBOX';
$imap_username = 'user';
$imap_password = 'password';
$db_hostname = 'localhost';
$db_username = 'dbuser';
$db_password = 'dbpassword';
$db_base = 'dbname';
// counter of total messages to be processed
$messages_to_process = 0;
// if something goes wrong, this script may be re-run with id of last processed message
$tocontinue = 0;
if(isset($argv[1]))
{
$tocontinue = $argv[1];
}
/* try to connect */
imap_timeout(IMAP_READTIMEOUT, 360);
imap_timeout(IMAP_OPENTIMEOUT, 360);
$inbox = imap_open($imap_hostname, $imap_username, $imap_password, OP_SHORTCACHE) or die('Cannot connect to imap: ' . imap_last_error());
// remove messages marked for deletion, may be slow:
imap_expunge($inbox);
$dblink = mysqli_connect($db_hostname, $db_username, $db_password, $db_base);
if (mysqli_connect_errno()) {
printf("MySQL connect failed: %s\n", mysqli_connect_error());
exit();
}
/* change dblink character set to utf8 */
if (!mysqli_set_charset($dblink, "utf8"))
{
printf("Error loading character set utf8: %s\n", mysqli_error($dblink));
}
else
{
printf("Current character set: %s\n", mysqli_character_set_name($dblink));
}
echo "Connected!\n";
// messages in imap folder
$num_mgs = imap_num_msg($inbox);
$messages_to_process = $num_mgs;
echo "$num_mgs messages to read\n";
/* grab emails */
$emails = imap_search($inbox,'ALL', SE_UID);
if($emails === false)
{
die('Could not perform imap search: '. imap_last_error());
}
// if argument is supplied, continue from requested point
if($tocontinue != 0)
{
$tocontinue = array_search($tocontinue, $emails);
$messages_to_process = $num_mgs - $tocontinue;
}
/* if emails are returned, cycle through each */
/* for every email... */
for($curid = $tocontinue; $curid < sizeof($emails); $curid++)
{
// dirty code. does garbage cleaning every 15 iterations, otherwise script consumes HUGE amounts of memory
if(version_compare(PHP_VERSION, '5.3.0', '>=') && $curid % 15 == 0)
{
imap_gc($inbox, IMAP_GC_ELT);
}
$email_number = $emails[$curid];
// check every 90 iterations if imap is still alive, may be slow
if($curid % 90 == 0 && !imap_ping ($inbox))
{
die("Imap is dead");
}
// $email_number = $messages_to_process;
$messages_to_process--;
print "-------------------------------------------------\n";
/* get information specific to this email */
//$overview = imap_fetch_overview($inbox,$email_number,0);
//$struct = imap_fetchstructure($inbox,$email_number);
$rheaders = imap_fetchheader($inbox, $email_number, FT_UID);
$pheaders = imap_rfc822_parse_headers($rheaders);
// FIXME: wtf?!
if(empty($pheaders->sender) && empty($pheaders->to))
{
echo "rh: "; print_r($rheaders); echo "\n";
echo "ph: "; print_r($pheaders); echo "\n";
echo "ib: "; print_r($inbox); echo "\n";
printf("FAILURE: %d!\n", $email_number);
continue;
}
printf("UID being processed: %s\n", $email_number);
/* getting email subject, some dirty code, may be slow */
$ft_subj = decodeHeader(imap_utf8($pheaders->subject));
//$ft_subj = imap_utf8($pheaders->subject);
print 'subj: ' . $ft_subj . "\n";
/* getting email date */
$ft_date = $pheaders->date;
print 'date: ' . $ft_date . "\n";
/* getting sender and recipients */
if (!is_array($pheaders->sender) || count($pheaders->sender) < 1)
{
print_r($pheaders);
printf("\n");
die("Something is wrong with SENDER address\n");
}
$ft_sender = $pheaders->sender[0]->mailbox . "@" . $pheaders->sender[0]->host;
print ' sender: ' . $ft_sender . "\n";
// collect message recepients data (to, cc, bcc)
$receivers = array();
if (!is_array($pheaders->to) || count($pheaders->to) < 1)
{
print_r($pheaders);
printf("\n");
// die("Something is totally wrong with TO addresses\n"); // may happen when there's only CC address
}
if(is_array($pheaders->to))
{
$receivers = $pheaders->to;
}
if (isset($pheaders->cc) && is_array($pheaders->cc) && count($pheaders->cc) > 0)
{
$receivers = array_merge($receivers, $pheaders->cc);
}
if (isset($pheaders->bcc) && is_array($pheaders->bcc) && count($pheaders->bcc) > 0)
{
$receivers = array_merge($receivers, $pheaders->bcc);
}
foreach ($receivers as $id => $val)
{
// echo " to: " . $val->mailbox . "@" . $val->host . "\n";
$receivers[$id] = $val->mailbox . "@" . $val->host;
}
/* make comma-separated list of recipients */
$ft_receivers = implode(', ', $receivers);
print "to: " . $ft_receivers . "\n";
/* email body */
$ft_data = $rheaders . imap_body($inbox,$email_number, FT_UID | FT_PEEK | FT_INTERNAL);
$ft_date_converted = date("Y-m-d g:i:s a", strtotime($ft_date));
/* inserting all collected values to database */
$query = "INSERT into `mailarch_def` (`from`, `to`, `subj`, `date`, `data`) VALUES (?,?,?,?,?)";
$stmt = mysqli_prepare($dblink, $query);
mysqli_stmt_bind_param($stmt, "sssss",
$ft_sender,
$ft_receivers,
$ft_subj,
$ft_date_converted,
$ft_data
);
/* execute prepared statement */
mysqli_stmt_execute($stmt);
/* check result. in this case error is triggered mostly by unique keys in db. it is ok */
$aff_rows = mysqli_stmt_affected_rows($stmt);
($aff_rows > 0) ? printf("%d Row inserted\n", $aff_rows) : printf("Duplicate or error.\n". mysqli_error($dblink)."\n");
printf("%d Messages to process\n", $messages_to_process);
/* close statement and connection */
mysqli_stmt_close($stmt);
/* WARNING! to remove processed messages uncomment the following line */
//imap_delete($inbox, $email_number, FT_UID);
/* this was a test, it should stay commented */
//imap_setflag_full($inbox, $email_number, '\Deleted', FT_UID);
}
/* close connections */
imap_close($inbox);
mysqli_close($dblink);
exit('Done!');
/* workaround to make most of headers to parse properly */
function decodeHeader($hdr, $cset = 'UTF8')
{
// Copied nearly intact from PEAR's Mail_mimeDecode.
$hdr = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $hdr);
$m = array();
if(is_array($hdr))
$hdr = $hdr[0];
while(preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $hdr, $m))
{
$encoded = $m[1];
$charset = strtoupper($m[2]);
$encoding = strtolower($m[3]);
$text = $m[4];
switch($encoding)
{
case 'b':
$text = base64_decode($text);
break;
case 'q':
$text = str_replace('_', ' ', $text);
preg_match_all('/=([a-f0-9]{2})/i', $text, $m);
foreach($m[1] as $value)
$text = str_replace('=' . $value, chr(hexdec($value)), $text);
break;
}
if($charset !== $cset)
$text = charconv($charset, $cset, $text);
$hdr = str_replace($encoded, $text, $hdr);
}
return $hdr;
}
/* workaround to make most of headers to parse properly */
function charconv($enc_from, $enc_to, $text)
{
if(function_exists('iconv'))
return iconv($enc_from, $enc_to, $text);
elseif(function_exists('recode_string'))
return recode_string("$enc_from..$enc_to", $text);
elseif(function_exists('mb_convert_encoding'))
return mb_convert_encoding($text, $enc_to, $enc_from);
return $text;
}
?>