• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-server / 476c8f25-8ad0-4d8b-bc39-a149b8c9164f

06 Jul 2024 03:12PM UTC coverage: 94.216% (-0.01%) from 94.226%
476c8f25-8ad0-4d8b-bc39-a149b8c9164f

push

circleci

edwh
Fix server hero calculations.

25200 of 26747 relevant lines covered (94.22%)

31.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.99
/include/mail/Digest.php
1
<?php
2
namespace Freegle\Iznik;
3

4

5
require_once(IZNIK_BASE . '/mailtemplates/digest/off.php');
1✔
6
require_once(IZNIK_BASE . '/lib/GreatCircle.php');
1✔
7

8
class Digest
9
{
10
    /** @var  $dbhr LoggedPDO */
11
    private $dbhr;
12
    /** @var  $dbhm LoggedPDO */
13
    private $dbhm;
14

15
    private $errorlog;
16

17
    const NEVER = 0;
18
    const IMMEDIATE = -1;
19
    const HOUR1 = 1;
20
    const HOUR2 = 2;
21
    const HOUR4 = 4;
22
    const HOUR8 = 8;
23
    const DAILY = 24;
24

25
    const SPOOLERS = 10;
26
    const SPOOLNAME = '/spool_';
27

28
    function __construct($dbhr, $dbhm, $id = NULL, $errorlog = FALSE)
29
    {
30
        $this->dbhr = $dbhr;
9✔
31
        $this->dbhm = $dbhm;
9✔
32
        $this->log = new Log($this->dbhr, $this->dbhm);
9✔
33
        $this->errorlog = $errorlog;
9✔
34
        
35
        $this->freqText = [
9✔
36
            Digest::NEVER => 'never',
9✔
37
            Digest::IMMEDIATE => 'immediately',
9✔
38
            Digest::HOUR1 => 'every hour',
9✔
39
            Digest::HOUR2 => 'every two hours',
9✔
40
            Digest::HOUR4 => 'every four hours',
9✔
41
            Digest::HOUR8 => 'every eight hours',
9✔
42
            Digest::DAILY => 'daily'
9✔
43
        ];
9✔
44
    }
45

46
    # Split out for UT to override
47
    public function sendOne($mailer, $message) {
48
        $mailer->send($message);
2✔
49
    }
50

51
    public function off($uid, $groupid) {
52
        $u = User::get($this->dbhr, $this->dbhm, $uid);
1✔
53

54
        if ($u->getId() == $uid) {
1✔
55
            if ($u->isApprovedMember($groupid)) {
1✔
56
                $u->setMembershipAtt($groupid, 'emailfrequency', 0);
1✔
57
                $g = Group::get($this->dbhr, $this->dbhm, $groupid);
1✔
58

59
                # We can receive messages for emails from the old system where the group id is no longer valid.
60
                if ($g->getId() == $groupid) {
1✔
61
                    $groupname = $g->getPublic()['namedisplay'];
1✔
62

63
                    $this->log->log([
1✔
64
                        'type' => Log::TYPE_USER,
1✔
65
                        'subtype' => Log::SUBTYPE_MAILOFF,
1✔
66
                        'user' => $uid,
1✔
67
                        'groupid' => $groupid
1✔
68
                    ]);
1✔
69

70
                    $email = $u->getEmailPreferred();
1✔
71
                    if ($email) {
1✔
72
                        list ($transport, $mailer) = Mail::getMailer();
1✔
73
                        $html = digest_off(USER_SITE, USERLOGO, $groupname);
1✔
74

75
                        $message = \Swift_Message::newInstance()
1✔
76
                            ->setSubject("Email Change Confirmation")
1✔
77
                            ->setFrom([NOREPLY_ADDR => 'Do Not Reply'])
1✔
78
                            ->setReturnPath("bounce-$uid-" . time() . "@" . USER_DOMAIN)
1✔
79
                            ->setTo([$email => $u->getName()])
1✔
80
                            ->setBody("We've turned your emails off on $groupname.");
1✔
81

82
                        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
83
                        # Outlook.
84
                        $htmlPart = \Swift_MimePart::newInstance();
1✔
85
                        $htmlPart->setCharset('utf-8');
1✔
86
                        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
87
                        $htmlPart->setContentType('text/html');
1✔
88
                        $htmlPart->setBody($html);
1✔
89
                        $message->attach($htmlPart);
1✔
90

91
                        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::DIGEST_OFF, $uid);
1✔
92

93
                        $this->sendOne($mailer, $message);
1✔
94
                    }
95
                }
96
            }
97
        }
98
    }
99

100
    private function possibleExternalImage($atts, $size) {
101
        # If the image is externally hosted, then we can request a smaller version.  For
102
        # immediate mails we show it full width.
103
        $image = NULL;
4✔
104

105
        if (count($atts) > 0) {
4✔
106
            $image = $atts[0]['path'];
×
107

108
            if (strpos($image, UPLOADCARE_CDN) !== FALSE) {
×
109
                $image .= "-/scale_crop/{$size}x{$size}/center/";
×
110
            }
111
        }
112

113
        return $image;
4✔
114
    }
115

116
    public function send($groupid, $frequency, $host = 'localhost', $uidforce = NULL, $allownearby = FALSE, $nearbyintext = FALSE) {
117
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
8✔
118
        $twig = new \Twig_Environment($loader);
8✔
119
        $sent = 0;
8✔
120

121
        $g = Group::get($this->dbhr, $this->dbhm, $groupid);
8✔
122

123
        # Don't send digests for closed groups.
124
        if (!$g->getSetting('closed', FALSE)) {
8✔
125
            $gatts = $g->getPublic();
8✔
126
            $sponsors = $g->getSponsorships();
8✔
127

128
            if ($this->errorlog) { error_log("#$groupid " . $g->getPrivate('nameshort') . " send emails for $frequency"); }
8✔
129

130
            # Make sure we have a tracking entry.
131
            $sql = "INSERT IGNORE INTO groups_digests (groupid, frequency) VALUES (?, ?);";
8✔
132
            $this->dbhm->preExec($sql, [ $groupid, $frequency ]);
8✔
133

134
            $sql = "SELECT TIMESTAMPDIFF(MINUTE, started, NOW()) AS timeago, groups_digests.* FROM groups_digests WHERE groupid = ? AND frequency = ? " . ($uidforce ? '' : 'HAVING frequency = -1 OR timeago IS NULL OR timeago >= frequency * 60') . ";";
8✔
135
            #error_log("Look for groups to process $sql, $groupid, $frequency");
136
            $tracks = $this->dbhr->preQuery($sql, [ $groupid, $frequency ]);
8✔
137

138
            $tz1 = new \DateTimeZone('UTC');
8✔
139
            $tz2 = new \DateTimeZone('Europe/London');
8✔
140

141
            foreach ($tracks as $track) {
8✔
142
                if ($this->errorlog) { error_log("Start group $groupid"); }
8✔
143
                $sql = "UPDATE groups_digests SET started = NOW() WHERE groupid = ? AND frequency = ?;";
8✔
144
                $this->dbhm->preExec($sql, [$groupid, $frequency]);
8✔
145

146
                # Find the cut-off time for the earliest message we want to include.  If we've not sent anything for this
147
                # group/frequency before then ensure we don't send anything older than a day the first time. And never
148
                # send anything older than 30 days, that's just silly.
149
                $oldest  = Utils::pres('ended', $track) ? '' : " AND messages_groups.arrival >= '" . date("Y-m-d H:i:s", strtotime("24 hours ago")) . "'";
8✔
150
                $oldest .=  " AND messages_groups.arrival >= '" . date("Y-m-d H:i:s", strtotime("30 days ago")) . "'";
8✔
151

152
                # We record where we got up to using arrival.  We don't use msgid because the arrival gets reset when
153
                # we repost, but the msgid remains the same, and we want to send out messages which have been reposted
154
                # here.
155
                #
156
                # arrival is a high-precision timestamp, so it's effectively unique per message.
157
                #
158
                # We don't want to mail messages from deleted users.
159
                $msgdtq = $track['msgdate'] ? " AND messages_groups.arrival > '{$track['msgdate']}' " : '';
8✔
160

161
                # If we're forcing, change the query so that we get a message to send.
162
                $limq = $uidforce ? " LIMIT 20 " : '';
8✔
163
                $ord = $uidforce ? " DESC " : " ASC ";
8✔
164
                $oldest = $uidforce ? '' : $oldest;
8✔
165
                $msgdtq = $uidforce ? '' : $msgdtq;
8✔
166

167
                $sql = "SELECT msgid, messages_groups.arrival, autoreposts FROM messages_groups
8✔
168
                        INNER JOIN messages ON messages.id = messages_groups.msgid
169
                        INNER JOIN users ON users.id = messages.fromuser
170
                        WHERE groupid = ? AND collection = ? AND messages_groups.deleted = 0 AND users.deleted IS NULL $oldest $msgdtq ORDER BY messages_groups.arrival $ord $limq;";
8✔
171
                $messages = $this->dbhr->preQuery($sql, [
8✔
172
                    $groupid,
8✔
173
                    MessageCollection::APPROVED,
8✔
174
                ]);
8✔
175

176
                $subjects = [];
8✔
177
                $available = [];
8✔
178
                $unavailable = [];
8✔
179
                $maxmsg = 0;
8✔
180
                $maxdate = NULL;
8✔
181

182
                foreach ($messages as $message) {
8✔
183
                    $maxmsg = max($message['msgid'], $maxmsg);
8✔
184

185
                    # Because we order by arrival, this will end up being the most recent message, i.e. max(arrival).
186
                    $maxdate = $message['arrival'];
8✔
187

188
                    $m = new Message($this->dbhr, $this->dbhm, $message['msgid']);
8✔
189
                    $subject = $m->getSubject();
8✔
190
                    $availablenow = $m->getPrivate('availablenow');
8✔
191

192
                    if ($availablenow > 1) {
8✔
193
                        # Include this in the subject line.
194
                        $subject .= " [$availablenow available]";
×
195
                    }
196

197
                    $subjects[$message['msgid']] = $subject;
8✔
198

199
                    $atts = $m->getPublic(FALSE, TRUE, TRUE);
8✔
200
                    $atts['autoreposts'] = $message['autoreposts'];
8✔
201
                    $atts['subject'] = $subject;
8✔
202
                    $atts['namedisplay'] = User::removeTNGroup($atts['namedisplay']);
8✔
203

204
                    $atts['firstposted'] = NULL;
8✔
205

206
                    if (count($atts['postings']) > 1) {
8✔
207
                        # This message has been reposted.
208
                        $datetime = new \DateTime('@' . strtotime($atts['date']), $tz1);
×
209
                        $datetime->setTimezone($tz2);
×
210
                        $atts['firstposted'] = $datetime->format('D, jS F g:ia');
×
211
                    }
212

213
                    # Strip out the clutter associated with various ways of posting.
214
                    $atts['textbody'] = $m->stripGumf();
8✔
215

216
                    if ($atts['type'] == Message::TYPE_OFFER || $atts['type'] == Message::TYPE_WANTED) {
8✔
217
                        if (count($atts['outcomes']) == 0) {
8✔
218
                            $available[] = $atts;
8✔
219
                        } else if (!Utils::presdef('firstposted', $atts, NULL)) {
1✔
220
                            $unavailable[] = $atts;
1✔
221
                        }
222
                    }
223
                }
224

225
                # Sort the messages so that new ones appear at the top.  This helps people who don't want to
226
                # scan through the old messages.
227
                usort($available, function($a, $b) {
8✔
228
                    if (Utils::pres('firstposted', $a) && !Utils::pres('firstposted', $b)) {
1✔
229
                        return 1;
×
230
                    } else if (!Utils::pres('firstposted', $a) && Utils::pres('firstposted', $b)) {
1✔
231
                        return -1;
×
232
                    } else {
233
                        return 0;
1✔
234
                    }
235
                });
8✔
236

237
                # Build the array of message(s) to send.  If we are sending immediately this may have multiple,
238
                # otherwise it'll just be one.
239
                #
240
                # We expand twig templates at this stage for fields which related to the message but not the
241
                # recipient.  Per-recipient fields are expanded using the Swift decorator later on, so for
242
                # those we expand them to a Swift variable.  That's a bit confusing but it means that
243
                # we don't expand the message variables more often than we need to, for performance reasons.
244
                $tosend = [];
8✔
245

246
                if ($frequency == Digest::IMMEDIATE) {
8✔
247
                    foreach ($available as $msg) {
4✔
248
                        # For immediate messages, which we send out as they arrive, we can set them to reply to
249
                        # the original sender.  We include only the text body of the message, because we
250
                        # wrap it up inside our own HTML.
251
                        #
252
                        # Anything that is per-group is passed in as a parameter here.  Anything that is or might
253
                        # become per-user is in the template as a {{...}} substitution.
254
                        $replyto = "replyto-{$msg['id']}-{{replyto}}@" . USER_DOMAIN;
4✔
255

256
                        $datetime = new \DateTime('@' . strtotime($msg['arrival']), $tz1);
4✔
257
                        $datetime->setTimezone($tz2);
4✔
258

259
                        try {
260
                            $html = $twig->render('digest/single.html', [
4✔
261
                                # Per-message fields for expansion now.  We serve the original image
262
                                # to avoid a resize operation cost.
263
                                'fromname' => $msg['fromname'] . ' on ' . SITE_NAME,
4✔
264
                                'subject' => $msg['subject'],
4✔
265
                                'textbody' => $msg['textbody'],
4✔
266
                                'image' => count($msg['attachments']) > 0 ? $msg['attachments'][0]['path'] : NULL,
4✔
267
                                'groupname' => $gatts['namedisplay'],
4✔
268
                                'replyweb' => "https://" . USER_SITE . "/message/{$msg['id']}",
4✔
269
                                'replyemail' => "mailto:$replyto?subject=" . rawurlencode("Re: " . $msg['subject']),
4✔
270
                                'date' => $datetime->format('D, jS F g:ia'),
4✔
271
                                'autoreposts' => $msg['autoreposts'],
4✔
272
                                'sponsors' => $sponsors,
4✔
273
                                'firstposted' => $msg['firstposted'],
4✔
274

275
                                # Per-recipient fields for later Swift expansion
276
                                'settings' => '{{settings}}',
4✔
277
                                'unsubscribe' => '{{unsubscribe}}',
4✔
278
                                'email' => '{{email}}',
4✔
279
                                'frequency' => '{{frequency}}',
4✔
280
                                'noemail' => '{{noemail}}',
4✔
281
                                'visit' => '{{visit}}',
4✔
282
                                'jobads' => '{{jobads}}',
4✔
283
                                'joblocation' => '{{joblocation}}'
4✔
284
                            ]);
4✔
285

286
                            $tosend[] = [
4✔
287
                                'subject' => '[' . $gatts['namedisplay'] . "] {$msg['subject']}",
4✔
288
                                'from' => $replyto,
4✔
289
                                'fromname' => $msg['fromname'] . ' on ' . SITE_NAME ,
4✔
290
                                'replyto' => $replyto,
4✔
291
                                'replytoname' => $msg['fromname'],
4✔
292
                                'html' => $html,
4✔
293
                                'text' => $msg['textbody']
4✔
294
                            ];
4✔
295
                        } catch (\Exception $e) {
×
296
                            error_log("Message prepare failed with " . $e->getMessage());
×
297
                        }
298
                    }
299
                } else if (count($available) + count($unavailable) > 0) {
4✔
300
                    # Build up the HTML for the message(s) in it.  We add a teaser of items to make it more
301
                    # interesting.
302
                    $textsumm = "Here are new posts or reposts since we last mailed you.\r\n\r\n";
4✔
303
                    $availablesumm = '';
4✔
304
                    $count = count($available) > 0 ? count($available) : 1;
4✔
305
                    $subject = "[{$gatts['namedisplay']}] What's New ($count message" .
4✔
306
                        ($count == 1 ? ')' : 's)');
4✔
307
                    $subjinfo = '';
4✔
308
                    $twigmsgsavail = [];
4✔
309
                    $twigmsgsunavail = [];
4✔
310

311
                    # Text TOC
312
                    foreach ($available as $msg) {
4✔
313
                        $textsumm .= $msg['subject'] . " at https://" . USER_SITE . "/message/{$msg['id']}\r\n\r\n";
4✔
314
                    }
315

316
                    $textsumm .= "----------------\r\n\r\n";
4✔
317

318
                    foreach ($available as $msg) {
4✔
319
                        $replyto = "replyto-{$msg['id']}-{{replyto}}@" . USER_DOMAIN;
4✔
320

321
                        $textsumm .= $msg['subject'] . " at \r\nhttps://" . USER_SITE . "/message/{$msg['id']}\r\n\r\n";
4✔
322
                        $textsumm .= $msg['textbody'] . "\r\n";
4✔
323
                        $textsumm .= "----------------\r\n\r\n";
4✔
324

325
                        $availablesumm .= $msg['subject'] . '<br />';
4✔
326

327
                        $twigmsgsavail[] = [
4✔
328
                            'id' => $msg['id'],
4✔
329
                            'subject' => $msg['subject'],
4✔
330
                            'textbody' => $msg['textbody'],
4✔
331
                            'fromname' => $msg['fromname'] . ' on ' . SITE_NAME,
4✔
332
                            'image' => $this->possibleExternalImage($msg['attachments'], 200),
4✔
333
                            'replyweb' => "https://" . USER_SITE . "/message/{$msg['id']}",
4✔
334
                            'replyemail' => "mailto:$replyto?subject=" . rawurlencode("Re: " . $msg['subject']),
4✔
335
                            'autoreposts' => $msg['autoreposts'],
4✔
336
                            'firstposted' => $msg['firstposted'],
4✔
337
                            'date' => date("D, jS F g:ia", strtotime($msg['arrival'])),
4✔
338
                        ];
4✔
339

340
                        list ($type, $item, $location ) = Message::parseSubject($msg['subject']);
4✔
341

342
                        if (strlen($item) < 25 && strlen($subjinfo) < 50) {
4✔
343
                            $subjinfo = $subjinfo == '' ? $item : "$subjinfo, $item";
3✔
344
                        }
345
                    }
346

347
                    if (!$subjinfo && count($available)) {
4✔
348
                        # Need something, at least.
349
                        list ($type, $item, $location ) = Message::parseSubject($available[0]['subject']);
1✔
350
                        $subjinfo = $item;
1✔
351
                    }
352

353
                    $textsumm .= "\r\n\r\nThese posts are new since your last mail but have already been completed. If you missed something, try changing how frequently we send you email in Settings.\r\n\r\n";
4✔
354

355
                    foreach ($unavailable as $msg) {
4✔
356
                        $textsumm .= $msg['subject'] . " at \r\nhttps://" . USER_SITE . "/message/{$msg['id']}\r\n\r\n";
1✔
357
                        $availablesumm .= $msg['subject'] . '<br />';
1✔
358

359
                        $twigmsgsunavail[] = [
1✔
360
                            'id' => $msg['id'],
1✔
361
                            'subject' => $msg['subject'],
1✔
362
                            'textbody' => $msg['textbody'],
1✔
363
                            'fromname' => $msg['fromname'],
1✔
364
                            'image' => $this->possibleExternalImage($msg['attachments'], 200),
1✔
365
                            'replyweb' => NULL,
1✔
366
                            'replyemail' => NULL,
1✔
367
                            'autoreposts' => $msg['autoreposts'],
1✔
368
                            'firstposted' => $msg['firstposted'],
1✔
369
                            'date' => date("D, jS F g:ia", strtotime($msg['arrival'])),
1✔
370
                        ];
1✔
371

372
                        $textsumm .= $msg['subject'] . " (post completed, no longer active)\r\n";
1✔
373
                    }
374

375
                    if ($subjinfo) {
4✔
376
                        $subject .= " - $subjinfo...";
4✔
377
                    }
378

379
                    try {
380
                        $html = $twig->render('digest/multiple.html', [
4✔
381
                            # Per-message fields for expansion now.
382
                            'groupname' => $gatts['namedisplay'],
4✔
383
                            'availablemessages'=> $twigmsgsavail,
4✔
384
                            'unavailablemessages'=> $twigmsgsunavail,
4✔
385
                            'previewtext' => $textsumm,
4✔
386

387
                            # Per-recipient fields for later Swift expansion
388
                            'settings' => '{{settings}}',
4✔
389
                            'unsubscribe' => '{{unsubscribe}}',
4✔
390
                            'email' => '{{email}}',
4✔
391
                            'frequency' => '{{frequency}}',
4✔
392
                            'noemail' => '{{noemail}}',
4✔
393
                            'visit' => '{{visit}}',
4✔
394
                            'jobads' => '{{jobads}}',
4✔
395
                            'sponsors' => $sponsors,
4✔
396
                            'joblocation' => '{{joblocation}}',
4✔
397
                            'nearby' => '{{nearby}}'
4✔
398
                        ]);
4✔
399
                    } catch (\Exception $e) {
×
400
                        error_log("Message prepare failed with " . $e->getMessage());
×
401
                    }
402

403
                    $tosend[] = [
4✔
404
                        'subject' => $subject,
4✔
405
                        'from' => $g->getAutoEmail(),
4✔
406
                        'fromname' => $gatts['namedisplay'],
4✔
407
                        'replyto' => $g->getModsEmail(),
4✔
408
                        'replytoname' => $gatts['namedisplay'],
4✔
409
                        'html' => $html,
4✔
410
                        'text' => $textsumm
4✔
411
                    ];
4✔
412
                }
413

414
                if (count($tosend) > 0) {
8✔
415
                    # Now find the users we want to send to on this group for this frequency.
416
                    $uidq = $uidforce ? " AND userid = $uidforce " : '';
8✔
417
                    $sql = "SELECT userid FROM memberships WHERE groupid = ? AND emailfrequency = ? $uidq ORDER BY userid ASC;";
8✔
418
                    $users = $this->dbhr->preQuery($sql,
8✔
419
                                                   [ $groupid, $frequency ]);
8✔
420

421
                    $replacements = [];
8✔
422
                    $emailToId = [];
8✔
423

424
                    foreach ($users as $user) {
8✔
425
                        # Keep connection alive.
426
                        $this->dbhm->preQuery("SELECT 1");
8✔
427

428
                        $u = User::get($this->dbhr, $this->dbhm, $user['userid']);
8✔
429
                        if ($this->errorlog) { error_log("Consider user {$user['userid']}"); }
8✔
430

431
                        # We are only interested in sending digests to users for whom we have a preferred address -
432
                        # otherwise where would we send them?  And not to TN members because they are just discarded
433
                        # there, so we are just wasting effort.
434
                        $email = $u->getEmailPreferred();
8✔
435
                        if ($this->errorlog) { error_log("Preferred $email, send ours " . $u->sendOurMails($g)); }
8✔
436

437
                        if ($email && $email != MODERATOR_EMAIL && !$u->isTN() && $u->sendOurMails($g)) {
8✔
438
                            $t = $u->loginLink(USER_SITE, $u->getId(), '/', User::SRC_DIGEST);
7✔
439
                            $creds = substr($t, strpos($t, '?'));
7✔
440

441
                            # We build up an array of the substitutions we need.
442
                            $jobads = $u->getJobAds();
7✔
443
                            $nearby = $allownearby ? $this->getMessagesOnNearbyGroups($twig, $u, $g, $frequency) : '';
7✔
444

445
                            $replacements[$email] = [
7✔
446
                                '{{uid}}' => $u->getId(),
7✔
447
                                '{{toname}}' => $u->getName(),
7✔
448
                                '{{bounce}}' => $u->getBounce(),
7✔
449
                                '{{settings}}' => $u->loginLink(USER_SITE, $u->getId(), '/settings', User::SRC_DIGEST),
7✔
450
                                '{{unsubscribe}}' => $u->loginLink(USER_SITE, $u->getId(), '/unsubscribe', User::SRC_DIGEST),
7✔
451
                                '{{email}}' => $email,
7✔
452
                                '{{frequency}}' => $this->freqText[$frequency],
7✔
453
                                '{{noemail}}' => 'digestoff-' . $user['userid'] . "-$groupid@" . USER_DOMAIN,
7✔
454
                                '{{post}}' => $u->loginLink(USER_SITE, $u->getId(), '/', User::SRC_DIGEST),
7✔
455
                                '{{visit}}' => $u->loginLink(USER_SITE, $u->getId(), '/browse', User::SRC_DIGEST),
7✔
456
                                '{{creds}}' => $creds,
7✔
457
                                '{{replyto}}' => $u->getId(),
7✔
458
                                '{{jobads}}' => $jobads['jobs'],
7✔
459
                                '{{joblocation}}' => $jobads['location'],
7✔
460
                                '{{nearby}}' => $nearby
7✔
461
                            ];
7✔
462

463
                            $emailToId[$email] = $u->getId();
7✔
464
                        }
465
                    }
466

467
                    if (count($replacements) > 0) {
8✔
468
                        error_log(date('Y-m-d H:i:s') . " #$groupid {$gatts['nameshort']} " . count($tosend) . " messages max $maxmsg, $maxdate to " . count($replacements) . " users");
7✔
469

470
                        # Now send.  We use a failover transport so that if we fail to send, we'll queue it for later
471
                        # rather than lose it.  We use multiple spoolers for throughput.
472
                        $spool = rand(1, self::SPOOLERS);
7✔
473
                        list ($transport, $mailer) = Mail::getMailer($host, self::SPOOLNAME . $spool);
7✔
474

475
                        # We're decorating using the information we collected earlier.  However the decorator doesn't
476
                        # cope with sending to multiple recipients properly (headers just get decorated with the first
477
                        # recipient) so we create a message for each recipient.
478
                        $decorator = new \Swift_Plugins_DecoratorPlugin($replacements);
7✔
479
                        $mailer->registerPlugin($decorator);
7✔
480

481
                        # We don't want to send too many mails before we reconnect.  This plugin breaks it up.
482
                        $mailer->registerPlugin(new \Swift_Plugins_AntiFloodPlugin(900));
7✔
483

484
                        $_SERVER['SERVER_NAME'] = USER_DOMAIN;
7✔
485
                        foreach ($tosend as $msg) {
7✔
486
                            foreach ($replacements as $email => $rep) {
7✔
487
                                try {
488
                                    # We created some HTML with logs of message links of this format:
489
                                    #   "https://" . USER_SITE . "/message/$msgid"
490
                                    # Add login info to them.
491
                                    # TODO This is a bit ugly.  Now that we send a single message per recipient is it
492
                                    # worth the double-loop we have in this function?
493
                                    $html = preg_replace('/(https:\/\/' . USER_SITE . '\/message\/[0-9]*)/', '$1' . $rep['{{creds}}'], $msg['html']);
7✔
494

495
                                    # If the text bodypart is empty, then it is omitted.  For mail clients set to display
496
                                    # the text bodypart, they may then make an attempt to display the HTML bodypart as
497
                                    # text, which looks wrong.  So make sure it's not empty.
498
                                    $msg['text'] = $msg['text'] ? $msg['text'] : '.';
7✔
499

500
                                    if ($nearbyintext) {
7✔
501
                                        # This is used in UT.
502
                                        $msg['text'] .= $rep['{{nearby}}'];
2✔
503
                                    }
504

505
                                    $message = \Swift_Message::newInstance()
7✔
506
                                        ->setSubject($msg['subject'] . ' ' . User::encodeId($emailToId[$email]))
7✔
507
                                        ->setFrom([$msg['from'] => $msg['fromname']])
7✔
508
                                        ->setReturnPath($rep['{{bounce}}'])
7✔
509
                                        ->setReplyTo($msg['replyto'], $msg['replytoname'])
7✔
510
                                        ->setBody($msg['text']);
7✔
511

512
                                    # Add HTML in base-64 as default quoted-printable encoding leads to problems on
513
                                    # Outlook.
514
                                    $htmlPart = \Swift_MimePart::newInstance();
7✔
515
                                    $htmlPart->setCharset('utf-8');
7✔
516
                                    $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
7✔
517
                                    $htmlPart->setContentType('text/html');
7✔
518

519
                                    # {{nearby}} doesn't expand correctly inside the decorator, so do it manually here.
520
                                    $htmlPart->setBody(str_replace('{{nearby}}', $rep['{{nearby}}'], $html));
7✔
521

522
                                    $message->attach($htmlPart);
7✔
523
                                    $message->setTo([ $email => $rep['{{toname}}'] ]);
7✔
524

525
                                    Mail::addHeaders($this->dbhr, $this->dbhm, $message,Mail::DIGEST, $rep['{{uid}}'], $frequency);
7✔
526

527
                                    #error_log("Send to $email");
528
                                    $this->sendOne($mailer, $message);
7✔
529
                                    $sent++;
6✔
530
                                } catch (\Exception $e) {
1✔
531
                                    error_log($email . " skipped with " . $e->getMessage());
1✔
532
                                }
533
                            }
534
                        }
535
                    }
536

537
                    if ($maxdate) {
8✔
538
                        # Record the message we got upto.
539
                        $sql = "UPDATE groups_digests SET msgid = ?, msgdate = ? WHERE groupid = ? AND frequency = ?;";
8✔
540
                        $this->dbhm->preExec($sql, [$maxmsg, $maxdate, $groupid, $frequency]);
8✔
541
                    }
542
                }
543

544
                $sql = "UPDATE groups_digests SET ended = NOW() WHERE groupid = ? AND frequency = ?;";
8✔
545
                $this->dbhm->preExec($sql, [$groupid, $frequency]);
8✔
546
            }
547
        }
548

549
        return($sent);
8✔
550
    }
551

552
    public function getMessagesOnNearbyGroups($twig, User $u, Group $g, $frequency) {
553
        $ret = '';
2✔
554

555
        if ($frequency != Digest::IMMEDIATE) {
2✔
556
            $nearby = $g->getSetting('nearbygroups', $g->defaultSettings['nearbygroups']);
2✔
557
            list ($lat, $lng, $loc) = $u->getLatLng(FALSE, FALSE);
2✔
558

559
            if ($nearby && ($lat || $lng)) {
2✔
560
                # The group we are mailing for allows us to show posts near the boundary.  Find extant messages on
561
                # other groups which are within this distance of the user, not from us and where we are not a
562
                # member of the group.  These are ones which might encourage us to join that group.
563
                $distance = $nearby * 1609.34;
2✔
564
                $ne = \GreatCircle::getPositionByDistance($distance, 45, $lat, $lng);
2✔
565
                $sw = \GreatCircle::getPositionByDistance($distance, 255, $lat, $lng);
2✔
566
                $box = "ST_GeomFromText('POLYGON(({$sw['lng']} {$sw['lat']}, {$sw['lng']} {$ne['lat']}, {$ne['lng']} {$ne['lat']}, {$ne['lng']} {$sw['lat']}, {$sw['lng']} {$sw['lat']}))', {$this->dbhr->SRID()})";
2✔
567

568
                $sql = "SELECT ST_Y(point) AS lat, ST_X(point) AS lng, messages_spatial.msgid, messages_spatial.groupid, messages.subject FROM messages_spatial 
2✔
569
    INNER JOIN messages ON messages_spatial.msgid = messages.id
570
    INNER JOIN users ON users.id = messages.fromuser
571
    LEFT JOIN memberships ON memberships.userid = ? AND memberships.groupid = messages_spatial.groupid
572
    LEFT JOIN messages_outcomes ON messages_spatial.msgid = messages_outcomes.msgid
573
    WHERE ST_Contains($box, point)
2✔
574
      AND messages_spatial.groupid != ? 
575
      AND fromuser != ?
576
      AND memberships.id IS NULL  
577
      AND messages_outcomes.id IS NULL
578
      AND users.deleted IS NULL
579
    ORDER BY messages_spatial.arrival ASC;";
2✔
580
                $posts = $this->dbhr->preQuery($sql, [
2✔
581
                    $u->getId(),
2✔
582
                    $g->getId(),
2✔
583
                    $u->getId()
2✔
584
                ]);
2✔
585

586
                $include = [];
2✔
587

588
                foreach ($posts as $post) {
2✔
589
                    # Get distance from user.
590
                    $away = \GreatCircle::getDistance($post['lat'], $post['lng'], $lat, $lng);
1✔
591

592
                    # We have searched using a box rather than circle, and the group the message is on might have a
593
                    # different distance.  So check both.
594
                    $g2 = Group::get($this->dbhr, $this->dbhm, $post['groupid']);
1✔
595
                    $nearby2 = $g2->getSetting('nearbygroups', $g->defaultSettings['nearbygroups']);
1✔
596
                    $distance2 = $nearby2 * 1609.34;
1✔
597
                    #error_log("Post is $away away, group limits $distance and $distance2");
598

599
                    if (($nearby2 && $away <= $distance) && ($away <= $distance2)) {
1✔
600
                        $now = microtime(TRUE);
1✔
601
                        error_log("$now Add nearby for user {$u->getId()} $lat,$lng group {$g->getId()} post {$post['msgid']} which is at {$post['lat']},{$post['lng']} distance away $away vs $distance/$distance2");
1✔
602
                        $post['replyweb'] = "https://" . USER_SITE . "/message/{$post['msgid']}?destinedfor=" . $u->getId() . "&timestamp=$now";
1✔
603
                        $include[] = $post;
1✔
604

605
                        if (count($include) >= 5) {
1✔
606
                            # Don't add too many, otherwise the mail gets stupid long.
607
                            break;
×
608
                        }
609
                    }
610
                }
611

612
                if (count($include)) {
2✔
613
                    // What we render here is not an entire message, it's a fragment that is then inserted.  So
614
                    // when converting from MJML we take care to extract the relevant part.
615
                    $ret = $twig->render('digest/nearby.html', [
1✔
616
                        'nearby' => $include
1✔
617
                    ]);
1✔
618
                }
619
            }
620
        }
621

622
        return $ret;
2✔
623
    }
624
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc