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

Freegle / iznik-server / c59981fe-c585-4ffc-9534-8ebcbd809988

07 Jul 2024 09:01PM UTC coverage: 94.216%. Remained the same
c59981fe-c585-4ffc-9534-8ebcbd809988

push

circleci

edwh
Use smaller images in digests now that we have a resizer.

0 of 1 new or added line in 1 file covered. (0.0%)

12 existing lines in 1 file now uncovered.

25200 of 26747 relevant lines covered (94.22%)

31.48 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
        $image = NULL;
8✔
102

103
        if (count($atts) > 0) {
8✔
UNCOV
104
            $image = $atts[0]['path'];
×
105

106
            if (strpos($image, UPLOADCARE_CDN) !== FALSE) {
×
107
                # If the image is externally hosted, then we can request a smaller version.  For
108
                # immediate mails we'd like to show it full width, whereas in digests we only need the
109
                # thumbnails.
NEW
110
                $image .= "-/scale_crop/{$size}x{$size}/center/";
×
111
            }
112
        }
113

114
        return $image;
8✔
115
    }
116

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

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

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

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

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

135
            $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✔
136
            #error_log("Look for groups to process $sql, $groupid, $frequency");
137
            $tracks = $this->dbhr->preQuery($sql, [ $groupid, $frequency ]);
8✔
138

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

354
                    $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✔
355

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

587
                $include = [];
2✔
588

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

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

600
                    if (($nearby2 && $away <= $distance) && ($away <= $distance2)) {
1✔
601
                        $now = microtime(TRUE);
1✔
602
                        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✔
603
                        $post['replyweb'] = "https://" . USER_SITE . "/message/{$post['msgid']}?destinedfor=" . $u->getId() . "&timestamp=$now";
1✔
604
                        $include[] = $post;
1✔
605

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

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

623
        return $ret;
2✔
624
    }
625
}
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