]> git.mxchange.org Git - friendica-addons.git/blob - mailstream/mailstream.php
67019f03c8e77a9df162131dc2b6b1e159379ea1
[friendica-addons.git] / mailstream / mailstream.php
1 <?php
2 /**
3  * Name: Mail Stream
4  * Description: Mail all items coming into your network feed to an email address
5  * Version: 2.0
6  * Author: Matthew Exon <http://mat.exon.name>
7  */
8
9 use Friendica\Content\Text\BBCode;
10 use Friendica\Core\Hook;
11 use Friendica\Core\Logger;
12 use Friendica\Core\Renderer;
13 use Friendica\Core\Worker;
14 use Friendica\Database\DBA;
15 use Friendica\DI;
16 use Friendica\Model\Item;
17 use Friendica\Model\Post;
18 use Friendica\Model\User;
19 use Friendica\Protocol\Activity;
20 use Friendica\Util\DateTimeFormat;
21
22 /**
23  * Sets up the addon hooks and the database table
24  */
25 function mailstream_install()
26 {
27         Hook::register('addon_settings', 'addon/mailstream/mailstream.php', 'mailstream_addon_settings');
28         Hook::register('addon_settings_post', 'addon/mailstream/mailstream.php', 'mailstream_addon_settings_post');
29         Hook::register('post_local_end', 'addon/mailstream/mailstream.php', 'mailstream_post_hook');
30         Hook::register('post_remote_end', 'addon/mailstream/mailstream.php', 'mailstream_post_hook');
31         Hook::register('cron', 'addon/mailstream/mailstream.php', 'mailstream_cron');
32         Hook::register('mailstream_send_hook', 'addon/mailstream/mailstream.php', 'mailstream_send_hook');
33
34         Logger::info("mailstream: installed");
35 }
36
37 /**
38  * Enforces that mailstream_install has set up the current version
39  */
40 function mailstream_check_version()
41 {
42         if (!is_null(DI::config()->get('mailstream', 'dbversion'))) {
43                 DI::config()->delete('mailstream', 'dbversion');
44                 Logger::info("mailstream_check_version: old version detected, reinstalling");
45                 mailstream_install();
46                 Hook::loadHooks();
47                 Hook::add(
48                         'mailstream_convert_table_entries',
49                         'addon/mailstream/mailstream.php',
50                         'mailstream_convert_table_entries'
51                 );
52                 Hook::fork(PRIORITY_LOW, 'mailstream_convert_table_entries');
53         }
54 }
55
56 /**
57  * This function indicates a module that can be wrapped in the LegacyModule class
58  */
59 function mailstream_module()
60 {
61 }
62
63 /**
64  * Adds an item in "addon features" in the admin menu of the site
65  *
66  * @param Friendica\App $a App object (unused)
67  * @param string        $o HTML form data
68  */
69 function mailstream_addon_admin(&$a, &$o)
70 {
71         $frommail = DI::config()->get('mailstream', 'frommail');
72         $template = Renderer::getMarkupTemplate('admin.tpl', 'addon/mailstream/');
73         $config = ['frommail',
74                         DI::l10n()->t('From Address'),
75                         $frommail,
76                         DI::l10n()->t('Email address that stream items will appear to be from.')];
77         $o .= Renderer::replaceMacros($template, [
78                                  '$frommail' => $config,
79                                  '$submit' => DI::l10n()->t('Save Settings')]);
80 }
81
82 /**
83  * Process input from the "addon features" part of the admin menu
84  */
85 function mailstream_addon_admin_post()
86 {
87         if (!empty($_POST['frommail'])) {
88                 DI::config()->set('mailstream', 'frommail', $_POST['frommail']);
89         }
90 }
91
92 /**
93  * Creates a message ID for a post URI in accordance with RFC 1036
94  * See also http://www.jwz.org/doc/mid.html
95  *
96  * @param string $uri the URI to be converted to a message ID
97  *
98  * @return string the created message ID
99  */
100 function mailstream_generate_id($uri)
101 {
102         $host = DI::baseUrl()->getHostname();
103         $resource = hash('md5', $uri);
104         $message_id = "<" . $resource . "@" . $host . ">";
105         Logger::debug('mailstream: Generated message ID ' . $message_id . ' for URI ' . $uri);
106         return $message_id;
107 }
108
109 function mailstream_send_hook(&$a, $data)
110 {
111         $criteria = array('uid' => $data['uid'], 'contact-id' => $data['contact-id'], 'uri' => $data['uri']);
112         $item = Post::selectFirst([], $criteria);
113         if (empty($item)) {
114                 Logger::error('mailstream_send_hook could not find item');
115                 return;
116         }
117
118         $user = User::getById($item['uid']);
119         if (empty($user)) {
120                         Logger::error('mailstream_send_hook could not fund user', ['uid' => $item['uid']]);
121                 return;
122         }
123
124         if (!mailstream_send($data['message_id'], $item, $user)) {
125                 Logger::debug('mailstream_send_hook send failed, will retry', $data);
126                 if (!Worker::defer()) {
127                         Logger::error('mailstream_send_hook failed and could not defer', $data);
128                 }
129         }
130 }
131
132 /**
133  * Called when either a local or remote post is created.  If
134  * mailstream is enabled and the necessary data is available, forks a
135  * workerqueue item to send the email.
136  *
137  * @param Friendica\App $a    App object (unused)
138  * @param array         $item content of the item (may or may not already be stored in the item table)
139  */
140 function mailstream_post_hook(&$a, &$item)
141 {
142         mailstream_check_version();
143
144         if (!DI::pConfig()->get($item['uid'], 'mailstream', 'enabled')) {
145                 Logger::debug('mailstream: not enabled for item ' . $item['id']);
146                 return;
147         }
148         if (!$item['uid']) {
149                 Logger::debug('mailstream: no uid for item ' . $item['id']);
150                 return;
151         }
152         if (!$item['contact-id']) {
153                 Logger::debug('mailstream: no contact-id for item ' . $item['id']);
154                 return;
155         }
156         if (!$item['uri']) {
157                 Logger::debug('mailstream: no uri for item ' . $item['id']);
158                 return;
159         }
160         if (!$item['plink']) {
161                 Logger::debug('mailstream: no plink for item ' . $item['id']);
162                 return;
163         }
164         if (DI::pConfig()->get($item['uid'], 'mailstream', 'nolikes')) {
165                 if ($item['verb'] == Activity::LIKE) {
166                         Logger::debug('mailstream: like item ' . $item['id']);
167                         return;
168                 }
169         }
170
171         $message_id = mailstream_generate_id($item['uri']);
172
173         $send_hook_data = array('uid' => $item['uid'],
174                                                                 'contact-id' => $item['contact-id'],
175                                                                 'uri' => $item['uri'],
176                                                                 'message_id' => $message_id,
177                                                                 'tries' => 0);
178         Hook::fork(PRIORITY_LOW, 'mailstream_send_hook', $send_hook_data);
179 }
180
181 /**
182  * If the user has configured attaching images to emails as
183  * attachments, this function searches the post for such images,
184  * retrieves the image, and inserts the data and metadata into the
185  * supplied array
186  *
187  * @param array         $item        content of the item
188  * @param array         $attachments contains an array element for each attachment to add to the email
189  *
190  * @return array new value of the attachments table (results are also stored in the reference parameter)
191  */
192 function mailstream_do_images(&$item, &$attachments)
193 {
194         if (!DI::pConfig()->get($item['uid'], 'mailstream', 'attachimg')) {
195                 return;
196         }
197         $attachments = [];
198         preg_match_all("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", $item["body"], $matches1);
199         preg_match_all("/\[img\](.*?)\[\/img\]/ism", $item["body"], $matches2);
200         preg_match_all("/\[img\=([^\]]*)\]([^[]*)\[\/img\]/ism", $item["body"], $matches3);
201         foreach (array_merge($matches1[3], $matches2[1], $matches3[1]) as $url) {
202                 $components = parse_url($url);
203                 if (!$components) {
204                         continue;
205                 }
206                 $cookiejar = tempnam(get_temppath(), 'cookiejar-mailstream-');
207                 $curlResult = DI::httpRequest()->fetchFull($url, 0, '', $cookiejar);
208                 $attachments[$url] = [
209                         'data' => $curlResult->getBody(),
210                         'guid' => hash("crc32", $url),
211                         'filename' => basename($components['path']),
212                         'type' => $curlResult->getContentType()
213                 ];
214
215                 if (strlen($attachments[$url]['data'])) {
216                         $item['body'] = str_replace($url, 'cid:' . $attachments[$url]['guid'], $item['body']);
217                         continue;
218                 }
219         }
220         return $attachments;
221 }
222
223 /**
224  * Creates a sender to use in the email, either from the contact or the author of the item, or both
225  *
226  * @param array $item content of the item
227  *
228  * @return string sender suitable for use in the email
229  */
230 function mailstream_sender($item)
231 {
232         $r = q('SELECT * FROM `contact` WHERE `id` = %d', $item['contact-id']);
233         if (DBA::isResult($r)) {
234                 $contact = $r[0];
235                 if ($contact['name'] != $item['author-name']) {
236                         return $contact['name'] . ' - ' . $item['author-name'];
237                 }
238         }
239         return $item['author-name'];
240 }
241
242 /**
243  * Converts a bbcode-encoded subject line into a plaintext version suitable for the subject line of an email
244  *
245  * @param string $subject bbcode-encoded subject line
246  *
247  * @return string plaintext subject line
248  */
249 function mailstream_decode_subject($subject)
250 {
251         $html = BBCode::convert($subject);
252         if (!$html) {
253                 return $subject;
254         }
255         $notags = strip_tags($html);
256         if (!$notags) {
257                 return $subject;
258         }
259         $noentity = html_entity_decode($notags);
260         if (!$noentity) {
261                 return $notags;
262         }
263         $nocodes = preg_replace_callback("/(&#[0-9]+;)/", function ($m) {
264                 return mb_convert_encoding($m[1], "UTF-8", "HTML-ENTITIES");
265         }, $noentity);
266         if (!$nocodes) {
267                 return $noentity;
268         }
269         $trimmed = trim($nocodes);
270         if (!$trimmed) {
271                 return $nocodes;
272         }
273         return $trimmed;
274 }
275
276 /**
277  * Creates a subject line to use in the email
278  *
279  * @param array $item content of the item
280  *
281  * @return string subject line suitable for use in the email
282  */
283 function mailstream_subject($item)
284 {
285         if ($item['title']) {
286                 return mailstream_decode_subject($item['title']);
287         }
288         $parent = $item['thr-parent'];
289         // Don't look more than 100 levels deep for a subject, in case of loops
290         for ($i = 0; ($i < 100) && $parent; $i++) {
291                 $parent_item = Post::selectFirst(['thr-parent', 'title'], ['uri' => $parent]);
292                 if (!DBA::isResult($parent_item)) {
293                         break;
294                 }
295                 if ($parent_item['thr-parent'] === $parent) {
296                         break;
297                 }
298                 if ($parent_item['title']) {
299                         return DI::l10n()->t('Re:') . ' ' . mailstream_decode_subject($parent_item['title']);
300                 }
301                 $parent = $parent_item['thr-parent'];
302         }
303         $r = q(
304                 "SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d",
305                 intval($item['contact-id']),
306                 intval($item['uid'])
307         );
308         if (!DBA::isResult($r)) {
309                         Logger::error(
310                                 'mailstream_subject no contact for item',
311                                 ['id' => $item['id'],
312                                   'plink' => $item['plink'],
313                                   'contact id' => $item['contact-id'],
314                                 'uid' => $item['uid']]
315                         );
316                 return DI::l10n()->t("Friendica post");
317         }
318         $contact = $r[0];
319         if ($contact['network'] === 'dfrn') {
320                 return DI::l10n()->t("Friendica post");
321         }
322         if ($contact['network'] === 'dspr') {
323                 return DI::l10n()->t("Diaspora post");
324         }
325         if ($contact['network'] === 'face') {
326                 $text = mailstream_decode_subject($item['body']);
327                 // For some reason these do show up in Facebook
328                 $text = preg_replace('/\xA0$/', '', $text);
329                 $subject = (strlen($text) > 150) ? (substr($text, 0, 140) . '...') : $text;
330                 return preg_replace('/\\s+/', ' ', $subject);
331         }
332         if ($contact['network'] === 'feed') {
333                 return DI::l10n()->t("Feed item");
334         }
335         if ($contact['network'] === 'mail') {
336                 return DI::l10n()->t("Email");
337         }
338         return DI::l10n()->t("Friendica Item");
339 }
340
341 /**
342  * Sends a message using PHPMailer
343  *
344  * @param string $message_id ID of the message (RFC 1036)
345  * @param array  $item       content of the item
346  * @param array  $user       results from the user table
347  *
348  * @return bool True if this message has been completed.  False if it should be retried.
349  */
350 function mailstream_send($message_id, $item, $user)
351 {
352         if (!is_array($item)) {
353                 Logger::error('mailstream_send item is empty', ['message_id' => $message_id]);
354                 return;
355         }
356
357         if (!$item['visible']) {
358                 Logger::debug('mailstream_send item not yet visible', ['item uri' => $item['uri']]);
359                 return false;
360         }
361         if (!$message_id) {
362                 Logger::error('mailstream_send no message ID supplied', ['item uri' => $item['uri'],
363                                 'user email' => $user['email']]);
364                 return true;
365         }
366         require_once(dirname(__file__).'/phpmailer/class.phpmailer.php');
367
368         $attachments = [];
369         mailstream_do_images($item, $attachments);
370         $frommail = DI::config()->get('mailstream', 'frommail');
371         if ($frommail == "") {
372                 $frommail = 'friendica@localhost.local';
373         }
374         $address = DI::pConfig()->get($item['uid'], 'mailstream', 'address');
375         if (!$address) {
376                 $address = $user['email'];
377         }
378         $mail = new PHPmailer;
379         try {
380                 $mail->XMailer = 'Friendica Mailstream Addon';
381                 $mail->SetFrom($frommail, mailstream_sender($item));
382                 $mail->AddAddress($address, $user['username']);
383                 $mail->MessageID = $message_id;
384                 $mail->Subject = mailstream_subject($item);
385                 if ($item['thr-parent'] != $item['uri']) {
386                         $mail->addCustomHeader('In-Reply-To: ' . mailstream_generate_id($item['thr-parent']));
387                 }
388                 $mail->addCustomHeader('X-Friendica-Mailstream-URI: ' . $item['uri']);
389                 $mail->addCustomHeader('X-Friendica-Mailstream-Plink: ' . $item['plink']);
390                 $encoding = 'base64';
391                 foreach ($attachments as $url => $image) {
392                         $mail->AddStringEmbeddedImage(
393                                 $image['data'],
394                                 $image['guid'],
395                                 $image['filename'],
396                                 $encoding,
397                                 $image['type']
398                         );
399                 }
400                 $mail->IsHTML(true);
401                 $mail->CharSet = 'utf-8';
402                 $template = Renderer::getMarkupTemplate('mail.tpl', 'addon/mailstream/');
403                 $mail->AltBody = BBCode::toPlaintext($item['body']);
404                 $item['body'] = BBCode::convert($item['body'], false, BBCode::CONNECTORS);
405                 $item['url'] = DI::baseUrl()->get() . '/display/' . $item['guid'];
406                 $mail->Body = Renderer::replaceMacros($template, [
407                                                  '$upstream' => DI::l10n()->t('Upstream'),
408                                                  '$local' => DI::l10n()->t('Local'),
409                                                  '$item' => $item]);
410                 mailstream_html_wrap($mail->Body);
411                 if (!$mail->Send()) {
412                         throw new Exception($mail->ErrorInfo);
413                 }
414                 Logger::debug('mailstream_send sent message', ['message ID' => $mail->MessageID,
415                                 'subject' => $mail->Subject,
416                                 'address' => $address]);
417         } catch (phpmailerException $e) {
418                 Logger::debug('mailstream_send PHPMailer exception sending message ' . $message_id . ': ' . $e->errorMessage());
419         } catch (Exception $e) {
420                 Logger::debug('mailstream_send exception sending message ' . $message_id . ': ' . $e->getMessage());
421         }
422
423         return true;
424 }
425
426 /**
427  * Email tends to break if you send excessively long lines.  To make
428  * bbcode's output suitable for transmission, we try to break things
429  * up so that lines are about 200 characters.
430  *
431  * @param string $text text to word wrap - modified in-place
432  */
433 function mailstream_html_wrap(&$text)
434 {
435         $lines = str_split($text, 200);
436         for ($i = 0; $i < count($lines); $i++) {
437                 $lines[$i] = preg_replace('/ /', "\n", $lines[$i], 1);
438         }
439         $text = implode($lines);
440 }
441
442 /**
443  * Convert v1 mailstream table entries to v2 workerqueue items
444  */
445 function mailstream_convert_table_entries()
446 {
447         $query = <<< EOT
448 SELECT
449   `message-id`,
450   `uri`,
451   `uid`,
452   `contact-id`
453 FROM
454    `mailstream_item`
455 WHERE
456   `mailstream_item`.`completed` IS NULL
457
458 EOT;
459         $ms_item_ids = q($query);
460         if (DBA::isResult($ms_item_ids)) {
461                 Logger::debug('mailstream_convert_table_entries processing ' . count($ms_item_ids) . ' items');
462                 foreach ($ms_item_ids as $ms_item_id) {
463                         $send_hook_data = array('uid' => $ms_item_id['uid'],
464                                                 'contact-id' => $ms_item_id['contact-id'],
465                                                 'uri' => $ms_item_id['uri'],
466                                                 'message_id' => $ms_item_id['message-id'],
467                                                 'tries' => 0);
468                         if (!$ms_item_id['message-id'] || !strlen($ms_item_id['message-id'])) {
469                                 Logger::info('mailstream_cron: Item ' .
470                                                                 $ms_item_id['id'] . ' URI ' . $ms_item_id['uri'] . ' has no message-id');
471                                                                 continue;
472                         }
473                         Logger::info('mailstream_convert_table_entries: convert item to workerqueue', $send_hook_data);
474                         Hook::fork(PRIORITY_LOW, 'mailstream_send_hook', $send_hook_data);
475                 }
476         }
477         q('DROP TABLE `mailstream_item`');
478 }
479
480 /**
481  * Form for configuring mailstream features for a user
482  *
483  * @param Friendica\App $a App object
484  * @param string        $o HTML form data
485  */
486 function mailstream_addon_settings(&$a, &$s)
487 {
488         $enabled = DI::pConfig()->get(local_user(), 'mailstream', 'enabled');
489         $address = DI::pConfig()->get(local_user(), 'mailstream', 'address');
490         $nolikes = DI::pConfig()->get(local_user(), 'mailstream', 'nolikes');
491         $attachimg= DI::pConfig()->get(local_user(), 'mailstream', 'attachimg');
492         $template = Renderer::getMarkupTemplate('settings.tpl', 'addon/mailstream/');
493         $s .= Renderer::replaceMacros($template, [
494                                  '$enabled' => [
495                                         'mailstream_enabled',
496                                         DI::l10n()->t('Enabled'),
497                                         $enabled],
498                                  '$address' => [
499                                         'mailstream_address',
500                                         DI::l10n()->t('Email Address'),
501                                         $address,
502                                         DI::l10n()->t("Leave blank to use your account email address")],
503                                  '$nolikes' => [
504                                         'mailstream_nolikes',
505                                         DI::l10n()->t('Exclude Likes'),
506                                         $nolikes,
507                                         DI::l10n()->t("Check this to omit mailing \"Like\" notifications")],
508                                  '$attachimg' => [
509                                         'mailstream_attachimg',
510                                         DI::l10n()->t('Attach Images'),
511                                         $attachimg,
512                                         DI::l10n()->t("Download images in posts and attach them to the email.  " .
513                                                                                                           "Useful for reading email while offline.")],
514                                  '$title' => DI::l10n()->t('Mail Stream Settings'),
515                                  '$submit' => DI::l10n()->t('Save Settings')]);
516 }
517
518 /**
519  * Process data submitted to user's mailstream features form
520  */
521 function mailstream_addon_settings_post()
522 {
523         if ($_POST['mailstream_address'] != "") {
524                 DI::pConfig()->set(local_user(), 'mailstream', 'address', $_POST['mailstream_address']);
525         } else {
526                 DI::pConfig()->delete(local_user(), 'mailstream', 'address');
527         }
528         if ($_POST['mailstream_nolikes']) {
529                 DI::pConfig()->set(local_user(), 'mailstream', 'nolikes', $_POST['mailstream_enabled']);
530         } else {
531                 DI::pConfig()->delete(local_user(), 'mailstream', 'nolikes');
532         }
533         if ($_POST['mailstream_enabled']) {
534                 DI::pConfig()->set(local_user(), 'mailstream', 'enabled', $_POST['mailstream_enabled']);
535         } else {
536                 DI::pConfig()->delete(local_user(), 'mailstream', 'enabled');
537         }
538         if ($_POST['mailstream_attachimg']) {
539                 DI::pConfig()->set(local_user(), 'mailstream', 'attachimg', $_POST['mailstream_attachimg']);
540         } else {
541                 DI::pConfig()->delete(local_user(), 'mailstream', 'attachimg');
542         }
543 }