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