Перекидывание почты в базу: PHP, IMAP, MySQL

Возникла необходимость перелить в базу данных содержимое почтового ящика. По получившемуся архиву должен осуществляться относительно быстрый поиск. Следующий скрипт забирает письма по IMAP, парсит их, помещает в MySQL, и при необходимости удаляет обработанную почту (отвечающий за удаление кусок кода нужно раскомментировать). Если удалять письма не нужно, то для более быстрой работы можно использовать опцию readonly при инициализации соединения с почтовым сервером.

В скрипте не предусмотрена обработка писем во вложенных папках, так как это не требовалось. Данные собираются в поля: дата, отправитель, получатели (to, cc и bcc скомбинированы в одну строку) и тема письма. Полная копия письма также сохраняется в BLOB.

Код писался на скорую руку и при определённых условиях фэйлит, но задачу свою он в итоге с нескольких попыток всё же выполнил. Так что отсюда можно выдрать несколько полезных моментов, в частности, обработку темы письма.

В случае остановки скрипта на произвольном месте, можно посмотреть в логе предыдущий обработанный UID письма и передать его в качестве параметра при следующем запуске, при этом обработка почты продолжится с указанного места.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#!/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;
	}
 
    ?>

2 thoughts on “Перекидывание почты в базу: PHP, IMAP, MySQL

    1. hermes Post author

      Самому, да. Это ж немного специфичная задачка, не всем и не каждый день нужно такими извращениями заниматься. Не помню что там было, но судя по скрипту, структура таблички примерно такая: varchar «from», varchar «to», varchar «subj», datetime «date», blob «data».
      Индексы тем более не помню какие были, но нужно было по комбинации from/to быстро искать.

      Reply

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *