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

Freegle / iznik-server / d488c654-e3b6-420a-85fc-51c31dfd41f1

15 Aug 2025 08:55AM UTC coverage: 90.409% (+0.004%) from 90.405%
d488c654-e3b6-420a-85fc-51c31dfd41f1

push

circleci

edwh
WIP: refactor phpunit tests to have less horrific amounts of duplicate code.

25943 of 28695 relevant lines covered (90.41%)

31.13 hits per line

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

92.61
/include/mail/MailRouter.php
1
<?php
2
namespace Freegle\Iznik;
3

4
use spamc;
5

6
if (!class_exists('spamc')) {
1✔
7
    require_once(IZNIK_BASE . '/lib/spamc.php');
1✔
8
}
9

10
# This class routes an incoming message
11
class MailRouter
12
{
13
    /** @var  $dbhr LoggedPDO */
14
    private $dbhr;
15
    /** @var  $dbhm LoggedPDO */
16
    private $dbhm;
17
    /** @var Message */
18
    private $msg;
19
    private $spamc;
20

21
    CONST ASSASSIN_THRESHOLD = 8;
22

23
    /**
24
     * @param LoggedPDO $dbhn
25
     */
26
    public function setDbhm($dbhm)
27
    {
28
        $this->dbhm = $dbhm;
1✔
29
    }
30

31
    private $spam;
32

33
    /**
34
     * @param mixed $spamc
35
     */
36
    public function setSpamc($spamc)
37
    {
38
        $this->spamc = $spamc;
3✔
39
    }
40

41
    const FAILURE = "Failure";
42
    const INCOMING_SPAM = "IncomingSpam";
43
    const APPROVED = "Approved";
44
    const PENDING = 'Pending';
45
    const TO_USER = "ToUser";
46
    const TO_SYSTEM ='ToSystem';
47
    const RECEIPT = 'ReadReceipt';
48
    const TRYST = 'Tryst';
49
    const DROPPED ='Dropped';
50
    const TO_VOLUNTEERS = "ToVolunteers";
51

52
    function __construct($dbhr, $dbhm, $id = NULL)
53
    {
54
        $this->dbhr = $dbhr;
187✔
55
        $this->dbhm = $dbhm;
187✔
56
        $this->log = new Log($this->dbhr, $this->dbhm);
187✔
57
        $this->spamc = new spamc;
187✔
58
        if (defined('SPAMD_HOST')) {
187✔
59
            $this->spamc->host = SPAMD_HOST;
187✔
60
        }
61
        if (defined('SPAMD_PORT')) {
187✔
62
            $this->spamc->port = SPAMD_PORT;
187✔
63
        }
64
        $this->spam = new Spam($this->dbhr, $this->dbhm);
187✔
65

66
        if ($id) {
187✔
67
            $this->msg = new Message($this->dbhr, $this->dbhm, $id);
13✔
68
        } else {
69
            $this->msg = new Message($this->dbhr, $this->dbhm);
175✔
70
        }
71
    }
72

73
    public function received($source, $from, $to, $msg, $groupid = NULL, $log = TRUE) {
74
        # We parse it and save it to the DB.  Then it will get picked up by background
75
        # processing.
76
        #
77
        # We have a groupid override because it's possible that we are syncing a message
78
        # from a group which has changed name and the To field might therefore not match
79
        # a current group name.
80
        $ret = NULL;
172✔
81
        $rc = $this->msg->parse($source, $from, $to, $msg, $groupid);
172✔
82

83
        if ($rc) {
172✔
84
            $ret = $this->msg->save($log);
172✔
85
        }
86
        
87
        return($ret);
172✔
88
    }
89

90
    # Public for UT
91
    public function markAsSpam($type, $reason) {
92
        return(
24✔
93
            $this->dbhm->preExec("UPDATE messages SET spamtype = ?, spamreason = ? WHERE id = ?;", [
24✔
94
                $type,
24✔
95
                $reason,
24✔
96
                $this->msg->getID()
24✔
97
            ]) &&
24✔
98
            $this->dbhm->preExec("UPDATE messages_groups SET collection = ? WHERE msgid = ?;", [
24✔
99
                MessageCollection::PENDING,
24✔
100
                $this->msg->getID()
24✔
101
            ]));
24✔
102
    }
103

104
    # Public for UT
105
    public function markApproved() {
106
        # Set this message to be in the Approved collection.
107
        # TODO Handle message on multiple groups
108
        $rc = $this->dbhm->preExec("UPDATE messages_groups SET collection = 'Approved', approvedat = NOW() WHERE msgid = ?;", [
105✔
109
            $this->msg->getID()
105✔
110
        ]);
105✔
111

112
        # Now visible in search
113
        $this->msg->index();
105✔
114

115
        return($rc);
105✔
116
    }
117

118
    # Public for UT
119
    public function markPending($force) {
120
        # Set the message as pending.
121
        #
122
        # If we're forced we just do it.  The force is to allow us to move from Spam to Pending.
123
        #
124
        # If we're not forced, then the mainline case is that this is an incoming message.  We might get a
125
        # pending notification after approving it, and in that case we don't generally want to move it back to
126
        # pending.  However if we approved/rejected it a while ago, then it's likely that the action didn't stick (for
127
        # example if we approved by email to Yahoo and Yahoo ignored it).  In that case we should move it
128
        # back to Pending, otherwise it will stay stuck on Yahoo.
129
        $overq = '';
37✔
130

131
        if (!$force) {
37✔
132
            $groups = $this->dbhr->preQuery("SELECT collection, approvedat, rejectedat FROM messages_groups WHERE msgid = ? AND ((collection = 'Approved' AND (approvedat IS NULL OR approvedat < DATE_SUB(NOW(), INTERVAL 2 HOUR))) OR (collection = 'Rejected' AND (rejectedat IS NULL OR rejectedat < DATE_SUB(NOW(), INTERVAL 2 HOUR))));",  [ $this->msg->getID() ]);
37✔
133
            $overq = count($groups) == 0 ? " AND collection = 'Incoming' " : '';
37✔
134
            #error_log("MarkPending " . $this->msg->getID() . " from collection $overq");
135
        }
136

137
        $rc = $this->dbhm->preExec("UPDATE messages_groups SET collection = 'Pending' WHERE msgid = ? $overq;", [
37✔
138
            $this->msg->getID()
37✔
139
        ]);
37✔
140

141
        # Notify mods of new work
142
        $groups = $this->msg->getGroups();
37✔
143
        $n = new PushNotifications($this->dbhr, $this->dbhm);
37✔
144

145
        foreach ($groups as $groupid) {
37✔
146
            $n->notifyGroupMods($groupid);
37✔
147
            error_log("Pending notify $groupid");
37✔
148
        }
149

150
        return($rc);
37✔
151
    }
152

153
    public function route($msg = NULL, $notspam = FALSE) {
154
        $ret = NULL;
182✔
155
        $log = TRUE;
182✔
156
        $keepgroups = FALSE;
182✔
157

158
        # We route messages to one of the following destinations:
159
        # - to a handler for system messages
160
        #   - confirmation of Yahoo mod status
161
        #   - confirmation of Yahoo subscription requests
162
        # - to a group, either pending or approved
163
        # - to group moderators
164
        # - to a user
165
        # - to a spam queue
166
        if ($msg) {
182✔
167
            $this->msg = $msg;
11✔
168
        }
169

170
        if ($notspam) {
182✔
171
            # Record that this message has been flagged as not spam.
172
            if ($this->log) { error_log("Record message as not spam"); }
1✔
173
            $this->msg->setPrivate('spamtype', Spam::REASON_NOT_SPAM, TRUE);
1✔
174
        }
175

176
        # Check if we know that this is not spam.  This means if we receive a later copy of it,
177
        # then we will know that we don't need to spam check it, otherwise we might move it back into spam
178
        # to the annoyance of the moderators.
179
        $notspam = $this->msg->getPrivate('spamtype') ==  Spam::REASON_NOT_SPAM;
182✔
180
        if ($this->log) { error_log("Consider not spam $notspam from " . $this->msg->getPrivate('spamtype')); }
182✔
181

182
        $to = $this->msg->getEnvelopeto();
182✔
183
        $from = $this->msg->getEnvelopefrom();
182✔
184
        $fromheader = $this->msg->getHeader('from');
182✔
185

186
        # TN authenticates mails with a secret header which we can use to skip spam checks.
187
        $tnsecret = $this->msg->getHeader('x-trash-nothing-secret');
182✔
188
        $notspam = $tnsecret == TNSECRET ? TRUE : $notspam;
182✔
189

190
        if ($fromheader) {
182✔
191
            $fromheader = mailparse_rfc822_parse_addresses($fromheader);
182✔
192
        }
193

194
        if ($this->spam->isSpammer($from))
182✔
195
        {
196
            # Mail from spammer. Drop it.
197
            if ($log) { error_log("Spammer, drop"); }
1✔
198
            $ret = MailRouter::DROPPED;
1✔
199
        } else if (strpos($to, 'info@twitter.com') !== FALSE) {
182✔
200
            # Twitter mails are getting us onto blacklists if we relay them.
201
            if ($log) { error_log("Twitter info, drop"); }
×
202
            $ret = MailRouter::DROPPED;
×
203
        } else if (strpos($to, FBL_ADDR) === 0) {
182✔
204
            $ret = $this->FBL();
1✔
205
        } else if (preg_match('/digestoff-(.*)-(.*)@/', $to, $matches) == 1) {
181✔
206
            $ret = $this->turnDigestOff($matches, $ret);
1✔
207
        } else if (preg_match('/readreceipt-(.*)-(.*)-(.*)@/', $to, $matches) == 1) {
180✔
208
            $ret = $this->readReceipt($matches);
1✔
209
        } else if (preg_match('/handover-(.*)-(.*)@/', $to, $matches) == 1) {
179✔
210
            $ret = $this->trystResponse($matches);
1✔
211
        } else if (preg_match('/eventsoff-(.*)-(.*)@/', $to, $matches) == 1) {
178✔
212
            $ret = $this->turnEventsOff($matches, $ret);
1✔
213
        } else if (preg_match('/newslettersoff-(.*)@/', $to, $matches) == 1) {
177✔
214
            $ret = $this->turnNewslettersOff($matches[1], $ret);
1✔
215
        } else if (preg_match('/relevantoff-(.*)@/', $to, $matches) == 1) {
176✔
216
            $ret = $this->turnRelevantOff($matches[1], $ret);
1✔
217
        } else if (preg_match('/volunteeringoff-(.*)-(.*)@/', $to, $matches) == 1) {
175✔
218
            $ret = $this->turnVolunteeringOff($matches, $ret);
×
219
        } else if (preg_match('/notificationmailsoff-(.*)@/', $to, $matches) == 1) {
175✔
220
            $ret = $this->turnNotificationsOff($matches[1], $ret);
1✔
221
        } else if (preg_match('/(.*)-volunteers@' . GROUP_DOMAIN . '/', $to, $matches) ||
174✔
222
            preg_match('/(.*)-auto@' . GROUP_DOMAIN . '/', $to, $matches)) {
174✔
223
            $ret = $this->toVolunteers($to, $matches[1], $notspam);
6✔
224
        } else if (preg_match('/(.*)-subscribe@' . GROUP_DOMAIN . '/', $to, $matches)) {
168✔
225
            $ret = $this->subscribe($matches[1]);
3✔
226
        } else if (preg_match('/unsubscribe-(.*)-(.*)-(.*)@' . USER_DOMAIN . '/', $to, $matches)) {
168✔
227
            $ret = $this->oneClickUnsubscribe($matches[1], $matches[2], $matches[3]);
1✔
228
        } else if (preg_match('/(.*)-unsubscribe@' . GROUP_DOMAIN . '/', $to, $matches)) {
167✔
229
            $ret = $this->unsubscribe($matches[1]);
1✔
230
        } else {
231
            list($spamscore, $spamfound, $groups, $notspam, $ret) = $this->checkSpam(
167✔
232
                $log,
167✔
233
                $notspam,
167✔
234
                $ret
167✔
235
            );
167✔
236

237
            if ($spamfound && strpos($to, '@' . USER_DOMAIN) !== FALSE) {
167✔
238
                # Horrible spaghetti logic.  We found spam in a message which will end up going to chat, if it
239
                # has the right kind of address.  We don't want to junk it - we want to send it to review.  We will
240
                # check the spamfound flag below when creating the chat message.
241
                error_log("Spam, but destined for chat, continue");
1✔
242
                $ret = NULL;
1✔
243
            }
244

245
            if (!$ret) {
167✔
246
                # Not obviously spam.
247
                if ($log) { error_log("Not obviously spam, groups " . var_export($groups, TRUE)); }
155✔
248

249
                if (count($groups) > 0) {
155✔
250
                    $ret = $this->toGroup($log, $notspam, $groups, $fromheader[0]['address'], $to);
144✔
251
                } else {
252
                    # It's not to one of our groups - but it could be a reply to one of our users, in several ways:
253
                    # - to the reply address we put in our What's New mails
254
                    # - directly to their USER_DOMAIN address, which happens after their message has been posted
255
                    #   on a Yahoo group and we get a reply through that route
256
                    # - in response to an email chat notification, which happens as a result of subsequent
257
                    #   communications after the previous two
258
                    $u = User::get($this->dbhr, $this->dbhm);
31✔
259
                    $to = $this->msg->getEnvelopeto();
31✔
260
                    $to = $to ? $to : $this->msg->getHeader('to');
31✔
261
                    if ($log) { error_log("Look for reply to $to from " . $this->msg->getEnvelopeFrom()); }
31✔
262

263
                    if (strlen($this->msg->getEnvelopeto()) && $this->msg->getEnvelopeto() == $this->msg->getEnvelopefrom()) {
31✔
264
                        # Sending to yourself isn't a valid path, and is used by spammers.
265
                        if ($log) { error_log("Sending to self " . $this->msg->getEnvelopeto() . " vs " . $this->msg->getEnvelopefrom() . " - dropped "); }
1✔
266
                        $ret = MailRouter::DROPPED;
1✔
267
                    } else if (preg_match('/replyto-(.*)-(.*)' . USER_DOMAIN . '/', $to, $matches)) {
30✔
268
                        $ret = $this->replyToSingleMessage($matches, $log, $ret, $spamfound);
4✔
269
                    } else if (preg_match('/notify-(.*)-(.*)@/', $to, $matches)) {
27✔
270
                        $ret = $this->replyToChatNotification($matches, $log, $ret, $spamfound);
7✔
271
                    } else if (!$this->msg->isAutoreply()) {
21✔
272
                        $ret = $this->directMailToUser($u, $to, $log, $spamscore, $spamfound);
21✔
273
                    } else {
274
                        if ($log) { error_log("Auto-reply - drop"); }
×
275
                        $ret = MailRouter::DROPPED;
×
276
                    }
277
                }
278
            }
279
        }
280

281
        if ($ret != MailRouter::FAILURE && !$keepgroups) {
182✔
282
            # Ensure no message is stuck in incoming.
283
            $this->dbhm->preExec("DELETE FROM messages_groups WHERE msgid = ? AND collection = ?;", [
182✔
284
                $this->msg->getID(),
182✔
285
                MessageCollection::INCOMING
182✔
286
            ]);
182✔
287
        }
288

289
        $this->dbhm->preExec("UPDATE messages SET lastroute = ? WHERE id = ?;", [
182✔
290
            $ret,
182✔
291
            $this->msg->getID()
182✔
292
        ]);
182✔
293

294
        if ($ret == MailRouter::APPROVED ||
182✔
295
            $ret == MailRouter::PENDING ||
111✔
296
            $ret == MailRouter::INCOMING_SPAM) {
182✔
297
            # If we routed successfully then we need to save the attachments.  No point saving them before now
298
            # because we don't know that we actually need them.
299
            #
300
            # No need for chat messages to users or mods because they are handled inside addPhotosToChat.
301
            error_log("Saving attachments for {$this->msg->getID()} {$this->msg->getSubject()}");
156✔
302
            $this->msg->saveAttachments($this->msg->getID());
156✔
303
        } else if (count($this->msg->getParsedAttachments()) &&
58✔
304
            ($ret == MailRouter::FAILURE || $ret == MailRouter::RECEIPT || $ret == MailRouter::TRYST || $ret == MailRouter::DROPPED)
58✔
305
            ) {
306
            error_log("Discarding attachments for {$this->msg->getID()} {$this->msg->getSubject()}");
1✔
307
        }
308

309
        # Dropped messages will get tidied up by cron; we leave them around in case we need to
310
        # look at them for PD.
311
        error_log("Routed #" . $this->msg->getID(). " " . $this->msg->getMessageID() . " " . $this->msg->getEnvelopefrom() . " -> " . $this->msg->getEnvelopeto() . " " . $this->msg->getSubject() . " " . $ret);
182✔
312

313
        return($ret);
182✔
314
    }
315

316
    private function addPhotosToChat($rid) {
317
        $m = new ChatMessage($this->dbhr, $this->dbhm);
29✔
318
        $count = 0;
29✔
319

320
        # Save the attachments so that they get uploaded.  This is attached to the message object but we will
321
        # create new attachments below referencing the same uploaded item - effectively moving them.
322
        $this->msg->saveAttachments($this->msg->getID());
29✔
323
        $atts = $this->msg->getAttachments();
29✔
324

325
        foreach ($atts as $att) {
29✔
326
            $hash = $att->getHash();
2✔
327

328
            if ($hash == '61e4d4a2e4bb8a5d' || $hash == '61e4d4a2e4bb8a59') {
2✔
329
                # Images to suppress, e.g. our logo.
330
            } else {
331
                list ($mid, $banned) = $m->create($rid, $this->msg->getFromuser(), NULL, ChatMessage::TYPE_IMAGE, NULL, FALSE);
2✔
332

333
                if ($mid) {
2✔
334
                    $a = new Attachment($this->dbhr, $this->dbhm, NULL, Attachment::TYPE_CHAT_MESSAGE);
2✔
335
                    list ($aid2, $uid) = $a->create($mid, NULL, $att->getExternalUid(), $att->getExternalUrl(), TRUE, $att->getExternalMods(), $att->getHash());
2✔
336
                    $m->setPrivate('imageid', $aid2);
2✔
337
                    $att->delete();
2✔
338

339
                    # Check whether this hash has recently been used for lots of messages.  If so then flag
340
                    # the message for review.  We currently only do this for email (which comes through here)
341
                    # as spam is largely an email problem.
342
                    $used = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM chat_images 
2✔
343
                         INNER JOIN chat_messages ON chat_images.id = chat_messages.imageid 
344
                         WHERE hash = ? AND TIMESTAMPDIFF(HOUR, chat_messages.date, NOW()) <= ?;", [
2✔
345
                        $hash,
2✔
346
                        Spam::IMAGE_THRESHOLD_TIME
2✔
347
                    ]);
2✔
348

349
                    if ($used[0]['count'] > Spam::IMAGE_THRESHOLD) {
2✔
350
                        $m->setPrivate('reviewrequired', 1);
1✔
351
                        $m->setPrivate('reportreason', Spam::REASON_IMAGE_SENT_MANY_TIMES);
1✔
352
                    }
353

354
                    $count++;
2✔
355
                }
356
            }
357
        }
358

359
        return($count);
29✔
360
    }
361

362
    public function routeAll() {
363
        $msgs = $this->dbhr->preQuery("SELECT msgid FROM messages_groups WHERE collection = 'Incoming' AND deleted = 0;");
1✔
364
        foreach ($msgs as $m) {
1✔
365
            try {
366
                // @codeCoverageIgnoreStart This seems to be needed due to a presumed bug in phpUnit.  This line
367
                // doesn't show as covered even though the next one does, which is clearly not possible.
368
                $msg = new Message($this->dbhr, $this->dbhm, $m['msgid']);
1✔
369
                // @codeCoverageIgnoreEnd
370

371
                if (!$msg->getDeleted()) {
1✔
372
                    $this->route($msg);
1✔
373
                }
374
            } catch (\Exception $e) {
1✔
375
                # Ignore this and continue routing the rest.
376
                error_log("Route #" . $this->msg->getID() . " failed " . $e->getMessage() . " stack " . $e->getTraceAsString());
1✔
377
                if ($this->dbhm->inTransaction()) {
1✔
378
                    $this->dbhm->rollBack();
×
379
                }
380
            }
381
        }
382
    }
383

384
    public function mail($to, $from, $subject, $body, $type, $uid) {
385
        # None of these mails need tracking, so we don't call AddHeaders.
386
        list ($transport, $mailer) = Mail::getMailer();
5✔
387

388
        $message = \Swift_Message::newInstance()
5✔
389
            ->setSubject($subject)
5✔
390
            ->setFrom($from)
5✔
391
            ->setTo($to)
5✔
392
            ->setBody($body);
5✔
393

394
        Mail::addHeaders($this->dbhr, $this->dbhm, $message, $type, $uid);
5✔
395

396
        $mailer->send($message);
5✔
397
    }
398

399
    private function FBL() {
400
        if ($this->log) { error_log("FBL report"); }
1✔
401

402
        // Find the email address that the FBL was about, in a hacky regex way.
403
        $handled = FALSE;
1✔
404
        $msg = $this->msg->getMessage();
1✔
405
        $email = NULL;
1✔
406

407
        if (preg_match('/Original-Rcpt-To:(.*)/', $msg, $matches)) {
1✔
408
            $email = trim($matches[1]);
1✔
409
        } else if (preg_match('/X-Original-To:(.*);/', $msg, $matches)) {
×
410
            $email = trim($matches[1]);
×
411
        }
412

413
        if ($email) {
1✔
414
            $handled = TRUE;
1✔
415
            if ($this->log) { error_log("FBL report to $email"); }
1✔
416

417
            $u = new User($this->dbhr, $this->dbhm);
1✔
418
            $uid = $u->findByEmail($email);
1✔
419
            if ($this->log) { error_log("FBL report for $uid"); }
1✔
420

421
            if ($uid) {
1✔
422
                $u = User::get($this->dbhr, $this->dbhm, $uid);
1✔
423
                $u->setSimpleMail(User::SIMPLE_MAIL_NONE);
1✔
424
                $u->FBL();
1✔
425
            }
426
        }
427

428
        IF (!$handled) {
1✔
429
            if ($this->log) { error_log("FBL report not processed"); }
×
430

431
            $this->mail(
×
432
                'log@ehibbert.org.uk',
×
433
                NOREPLY_ADDR,
×
434
                "Unprocessed FBL report received",
×
435
                $this->msg->getMessage(),
×
436
                Mail::MODMAIL,
×
437
                0
×
438
            );
×
439
        }
440

441
        return MailRouter::TO_SYSTEM;
1✔
442
    }
443

444
    private function turnDigestOff($matches, $ret)
445
    {
446
        # Request to turn email off.
447
        $uid = intval($matches[1]);
1✔
448
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
449
        $groupid = intval($matches[2]);
1✔
450

451
        if ($uid && $groupid)
1✔
452
        {
453
            $d = new Digest($this->dbhr, $this->dbhm);
1✔
454
            $d->off($uid, $groupid);
1✔
455

456
            $ret = MailRouter::TO_SYSTEM;
1✔
457
        }
458
        return $ret;
1✔
459
    }
460

461
    private function readReceipt($matches)
462
    {
463
        # Read receipt
464
        $chatid = intval($matches[1]);
1✔
465
        $userid = intval($matches[2]);
1✔
466
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $userid;");
1✔
467
        $msgid = intval($matches[3]);
1✔
468

469
        # The receipt has seen this message, and the message has been seen by all people in the chat (because
470
        # we only generate these for user 2 user.
471
        $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid);
1✔
472
        if ($r->canSee($userid, false))
1✔
473
        {
474
            $r->updateRoster($userid, $msgid);
1✔
475
            $r->seenByAll($msgid);
1✔
476
        }
477

478
        $ret = MailRouter::RECEIPT;
1✔
479
        return $ret;
1✔
480
    }
481

482
    private function trystResponse($matches)
483
    {
484
        # Calendar response
485
        $trystid = intval($matches[1]);
1✔
486
        $userid = intval($matches[2]);
1✔
487
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $userid;");
1✔
488

489
        # Scan for a VCALENDAR attachment.
490
        $t = new Tryst($this->dbhr, $this->dbhm, $trystid);
1✔
491
        $rsp = Tryst::OTHER;
1✔
492

493
        foreach ($this->msg->getParsedAttachments() as $att)
1✔
494
        {
495
            $ct = $att->getContentType();
1✔
496

497
            if (strcmp('text/calendar', strtolower($ct)) === 0)
1✔
498
            {
499
                # We don't do a proper parse
500
                $vcal = strtolower($att->getContent());
1✔
501
                if (strpos($vcal, 'status:confirmed') !== false || strpos($vcal, 'status:tentative') !== false)
1✔
502
                {
503
                    $rsp = Tryst::ACCEPTED;
1✔
504
                } else
505
                {
506
                    if (strpos($vcal, 'status:cancelled') !== false)
1✔
507
                    {
508
                        $rsp = Tryst::DECLINED;
×
509
                    }
510
                }
511
            }
512
        }
513

514
        if ($rsp == Tryst::OTHER)
1✔
515
        {
516
            # Maybe they didn't put the VCALENDAR in.
517
            if (stripos($this->msg->getSubject(), 'accepted') !== false)
1✔
518
            {
519
                $rsp = Tryst::ACCEPTED;
1✔
520
            } else
521
            {
522
                if (stripos($this->msg->getSubject(), 'declined') !== false)
1✔
523
                {
524
                    $rsp = Tryst::DECLINED;
1✔
525
                }
526
            }
527
        }
528

529
        $t->response($userid, $rsp);
1✔
530

531
        $ret = MailRouter::TRYST;
1✔
532
        return $ret;
1✔
533
    }
534

535
    private function turnEventsOff($matches, $ret)
536
    {
537
        # Request to turn events email off.
538
        $uid = intval($matches[1]);
1✔
539
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
540
        $groupid = intval($matches[2]);
1✔
541

542
        if ($uid && $groupid)
1✔
543
        {
544
            $d = new EventDigest($this->dbhr, $this->dbhm);
1✔
545
            $d->off($uid, $groupid);
1✔
546

547
            $ret = MailRouter::TO_SYSTEM;
1✔
548
        }
549
        return $ret;
1✔
550
    }
551

552
    private function turnNewslettersOff($matches, $ret)
553
    {
554
        # Request to turn newsletters off.
555
        $uid = intval($matches);
1✔
556
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
557

558
        if ($uid)
1✔
559
        {
560
            $d = new Newsletter($this->dbhr, $this->dbhm);
1✔
561
            $d->off($uid);
1✔
562

563
            $ret = MailRouter::TO_SYSTEM;
1✔
564
        }
565
        return $ret;
1✔
566
    }
567

568
    private function turnRelevantOff($matches, $ret)
569
    {
570
        # Request to turn "interested in" off.
571
        $uid = intval($matches);
1✔
572
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
573

574
        if ($uid)
1✔
575
        {
576
            $d = new Relevant($this->dbhr, $this->dbhm);
1✔
577
            $d->off($uid);
1✔
578

579
            $ret = MailRouter::TO_SYSTEM;
1✔
580
        }
581
        return $ret;
1✔
582
    }
583

584
    private function turnVolunteeringOff($matches, $ret)
585
    {
586
        # Request to turn volunteering email off.
587
        $uid = intval($matches[1]);
×
588
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
×
589
        $groupid = intval($matches[2]);
×
590

591
        if ($uid && $groupid)
×
592
        {
593
            $d = new VolunteeringDigest($this->dbhr, $this->dbhm);
×
594
            $d->off($uid, $groupid);
×
595

596
            $ret = MailRouter::TO_SYSTEM;
×
597
        }
598
        return $ret;
×
599
    }
600

601
    private function turnNotificationsOff($matches, $ret)
602
    {
603
        # Request to turn notification email off.
604
        $uid = intval($matches);
1✔
605
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
606

607
        if ($uid)
1✔
608
        {
609
            $d = new Notifications($this->dbhr, $this->dbhm);
1✔
610
            $d->off($uid);
1✔
611

612
            $ret = MailRouter::TO_SYSTEM;
1✔
613
        }
614
        return $ret;
1✔
615
    }
616

617
    private function toVolunteers($to, $matches, $notspam)
618
    {
619
        # Mail to our owner address.  First check if it's spam according to SpamAssassin.
620
        if ($this->log) {
6✔
621
            error_log("To volunteers");
6✔
622
        }
623

624
        $this->spamc->command = 'CHECK';
6✔
625

626
        $ret = MailRouter::INCOMING_SPAM;
6✔
627

628
        if ($notspam || $this->spamc->filter($this->msg->getMessage()))
6✔
629
        {
630
            $spamscore = $this->spamc->result['SCORE'];
6✔
631

632
            if ($notspam || $spamscore < MailRouter::ASSASSIN_THRESHOLD)
6✔
633
            {
634
                # Now do our own checks.
635
                if ($this->log)
6✔
636
                {
637
                    error_log("Passed SpamAssassin $spamscore");
6✔
638
                }
639
                list ($rc, $reason) = $notspam ? [ FALSE, NULL] : $this->spam->checkMessage($this->msg);
6✔
640

641
                if (!$rc)
6✔
642
                {
643
                    # Don't pass on automated mails from ADMINs - there might be loads.
644
                    if ($notspam ||
6✔
645
                        (preg_match('/(.*)-volunteers@' . GROUP_DOMAIN . '/', $to) || !$this->msg->isBounce() && !$this->msg->isAutoreply()))
6✔
646
                    {
647
                        $ret = MailRouter::FAILURE;
6✔
648

649
                        # It's not.  Find the group
650
                        $g = new Group($this->dbhr, $this->dbhm);
6✔
651
                        $sn = $matches;
6✔
652

653
                        $gid = $g->findByShortName($sn);
6✔
654
                        if ($this->log)
6✔
655
                        {
656
                            error_log("Found $gid from $sn");
6✔
657
                        }
658

659
                        if ($gid)
6✔
660
                        {
661
                            # It's one of our groups.  Find the user this is from.
662
                            $envfrom = $this->msg->getFromaddr();
6✔
663
                            $u = new User($this->dbhr, $this->dbhm);
6✔
664
                            $uid = $u->findByEmail($envfrom);
6✔
665
                            $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
6✔
666

667
                            if ($this->log)
6✔
668
                            {
669
                                error_log("Found $uid from $envfrom");
6✔
670
                            }
671

672
                            # We should always find them as Message::parse should create them
673
                            if ($uid)
6✔
674
                            {
675
                                if ($this->log)
6✔
676
                                {
677
                                    error_log("From user $uid to group $gid");
6✔
678
                                }
679
                                $s = new Spam($this->dbhr, $this->dbhm);
6✔
680

681
                                # Filter out mail to volunteers from known spammers.
682
                                $ret = MailRouter::INCOMING_SPAM;
6✔
683
                                $spammers = $s->getSpammerByUserid($uid);
6✔
684

685
                                if (!$spammers)
6✔
686
                                {
687
                                    $ret = MailRouter::DROPPED;
6✔
688

689
                                    # Don't want to pass on OOF etc.
690
                                    if ($notspam || !$this->msg->isAutoreply())
6✔
691
                                    {
692
                                        # Create/get a chat between the sender and the group mods.
693
                                        $r = new ChatRoom($this->dbhr, $this->dbhm);
6✔
694
                                        $chatid = $r->createUser2Mod($uid, $gid);
6✔
695
                                        if ($this->log)
6✔
696
                                        {
697
                                            error_log("Chatid is $chatid");
6✔
698
                                        }
699

700
                                        # Now add this message into the chat.  Don't strip quoted as it might be useful.
701
                                        $textbody = $this->msg->getTextBody();
6✔
702

703
                                        # ...but we don't want the whole digest, if they sent that.
704
                                        if (preg_match('/(.*)^\s*On.*?-auto@' . GROUP_DOMAIN . '> wrote\:(\s*)/ms', $textbody, $matches)) {
6✔
705
                                            $textbody = $matches[1];
1✔
706
                                            $textbody .= "\r\n\r\n(Replied to digest)";
1✔
707
                                        }
708

709
                                        if (preg_match('/(.*)^\s*-----Original Message-----(\s*)/ms', $textbody, $matches)) {
6✔
710
                                            $textbody = $matches[1];
1✔
711
                                            $textbody .= "\r\n\r\n(Replied to digest)";
1✔
712
                                        }
713

714
                                        if (strlen($textbody)) {
6✔
715
                                            $m = new ChatMessage($this->dbhr, $this->dbhm);
5✔
716

717
                                            // Force to review so that we don't mail it before we've recorded that the
718
                                            // sender has seen it.
719
                                            list ($mid, $banned) = $m->create(
5✔
720
                                                $chatid,
5✔
721
                                                $uid,
5✔
722
                                                $textbody,
5✔
723
                                                ChatMessage::TYPE_DEFAULT,
5✔
724
                                                null,
5✔
725
                                                false,
5✔
726
                                                null,
5✔
727
                                                null,
5✔
728
                                                null,
5✔
729
                                                null,
5✔
730
                                                null,
5✔
731
                                                true
5✔
732
                                            );
5✔
733

734
                                            $r->updateRoster($uid, $mid);
5✔
735

736
                                            // Allow mailing to happen.
737
                                            $m->setPrivate('reviewrequired', 0);
5✔
738

739
                                            if ($this->log)
5✔
740
                                            {
741
                                                error_log("Created message $mid");
5✔
742
                                            }
743

744
                                            $m->chatByEmail($mid, $this->msg->getID());
5✔
745
                                        }
746

747
                                        # Add any photos.
748
                                        $this->addPhotosToChat($chatid);
6✔
749

750
                                        $ret = MailRouter::TO_VOLUNTEERS;
6✔
751
                                    }
752
                                }
753
                            }
754
                        }
755
                    } else {
756
                        if ($this->log) {
×
757
                            error_log("Automated reply from ADMIN - drop");
×
758
                        }
759
                        $ret = MailRouter::DROPPED;
6✔
760
                    }
761
                } else {
762
                    if ($this->log) {
3✔
763
                        error_log("Spam: " . var_export($reason, TRUE));
3✔
764
                    }
765
                }
766
            }
767
        }
768
        return $ret;
6✔
769
    }
770

771
    private function subscribe($name)
772
    {
773
        $ret = MailRouter::FAILURE;
3✔
774

775
        # Find the group
776
        $g = new Group($this->dbhr, $this->dbhm);
3✔
777
        $gid = $g->findByShortName($name);
3✔
778
        $g = new Group($this->dbhr, $this->dbhm, $gid);
3✔
779

780
        if ($gid)
3✔
781
        {
782
            # It's one of our groups.  Find the user this is from.
783
            $envfrom = $this->msg->getEnvelopeFrom();
3✔
784
            $u = new User($this->dbhr, $this->dbhm);
3✔
785
            $uid = $u->findByEmail($envfrom);
3✔
786

787
            if (!$uid)
3✔
788
            {
789
                # We don't know them yet.
790
                $uid = $u->create(
×
791
                    null,
×
792
                    null,
×
793
                    $this->msg->getFromname(),
×
794
                    "Email subscription from $envfrom to " . $g->getPrivate('nameshort')
×
795
                );
×
796
                $u->addEmail($envfrom, 0);
×
797
                $pw = $u->inventPassword();
×
798
                $u->addLogin(User::LOGIN_NATIVE, $uid, $pw);
×
799
                $u->welcome($envfrom, $pw);
×
800
            }
801

802
            $u = new User($this->dbhr, $this->dbhm, $uid);
3✔
803
            $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
3✔
804

805
            # We should always find them as Message::parse should create them
806
            if ($u->getId())
3✔
807
            {
808
                $u->addMembership($gid, User::ROLE_MEMBER, null, MembershipCollection::APPROVED, $envfrom);
3✔
809

810
                # Remove any email logs for this message - no point wasting space on keeping those.
811
                $this->log->deleteLogsForMessage($this->msg->getID());
3✔
812
                $ret = MailRouter::TO_SYSTEM;
3✔
813
            }
814
        }
815
        return $ret;
3✔
816
    }
817

818
    private function oneClickUnsubscribe($uid, $key, $type) {
819
        $ret = MailRouter::DROPPED;
1✔
820

821
        $u = new User($this->dbhr, $this->dbhm, $uid);
1✔
822

823
        # Prevent accidental unsubscription by mods.
824
        if (!$u->isModerator()) {
1✔
825
            # Validate the key to prevent spoof unsubscribes.
826
            $ukey = $u->getUserKey($uid);
1✔
827

828
            if ($key && !strcasecmp($ukey, $key)) {
1✔
829
                $u->limbo("Unsubscribed from $type");
1✔
830
                $ret = MailRouter::TO_SYSTEM;
1✔
831
            }
832
        }
833

834
        return $ret;
1✔
835
    }
836

837
    private function unsubscribe($name)
838
    {
839
        $ret = MailRouter::FAILURE;
1✔
840

841
        # Find the group
842
        $g = new Group($this->dbhr, $this->dbhm);
1✔
843
        $gid = $g->findByShortName($name);
1✔
844

845
        if ($gid)
1✔
846
        {
847
            # It's one of our groups.  Find the user this is from.
848
            $envfrom = $this->msg->getEnvelopeFrom();
1✔
849
            $u = new User($this->dbhr, $this->dbhm);
1✔
850
            $uid = $u->findByEmail($envfrom);
1✔
851
            $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
852

853
            if ($uid)
1✔
854
            {
855
                $u = new User($this->dbhr, $this->dbhm, $uid);
1✔
856
                $ret = MailRouter::DROPPED;
1✔
857

858
                if (!$u->isModOrOwner($gid))
1✔
859
                {
860
                    $u->removeMembership($gid, false, false, $envfrom);
1✔
861

862
                    # Remove any email logs for this message - no point wasting space on keeping those.
863
                    $this->log->deleteLogsForMessage($this->msg->getID());
1✔
864
                    $ret = MailRouter::TO_SYSTEM;
1✔
865
                }
866
            }
867
        }
868
        return $ret;
1✔
869
    }
870

871
    private function checkSpam($log, bool $notspam, $ret): array
872
    {
873
        # We use SpamAssassin to weed out obvious spam.  We only do a content check if the message subject line is
874
        # not in the standard format.  Most generic spam isn't in that format, and some of our messages
875
        # would otherwise get flagged - so this improves overall reliability.
876
        $contentcheck = !$notspam && !preg_match('/.*?\:(.*)\(.*\)/', $this->msg->getSubject());
167✔
877
        $spamscore = null;
167✔
878
        $spamfound = false;
167✔
879

880
        $groups = $this->msg->getGroups(false, false);
167✔
881
        #error_log("Got groups " . var_export($groups, TRUE));
882

883
        # Check if the group wants us to check for spam.
884
        foreach ($groups as $group)
167✔
885
        {
886
            $g = Group::get($this->dbhr, $this->dbhm, $group['groupid']);
154✔
887
            $defs = $g->getDefaults();
154✔
888
            $spammers = $g->getSetting('spammers', $defs['spammers']);
154✔
889
            $check = array_key_exists(
154✔
890
                'messagereview',
154✔
891
                $spammers
154✔
892
            ) ? $spammers['messagereview'] : $defs['spammers']['messagereview'];
154✔
893
            $notspam = $check ? $notspam : true;
154✔
894
            #error_log("Consider spam review $notspam from $check, " . var_export($spammers, TRUE));
895
        }
896

897
        if (!$notspam)
167✔
898
        {
899
            # First check if this message is spam based on our own checks.
900
            $rc = $this->spam->checkMessage($this->msg);
166✔
901
            if ($rc)
166✔
902
            {
903
                if (count($groups) > 0)
11✔
904
                {
905
                    foreach ($groups as $group)
7✔
906
                    {
907
                        $this->log->log([
7✔
908
                                            'type' => Log::TYPE_MESSAGE,
7✔
909
                                            'subtype' => Log::SUBTYPE_CLASSIFIED_SPAM,
7✔
910
                                            'msgid' => $this->msg->getID(),
7✔
911
                                            'text' => "{$rc[2]}",
7✔
912
                                            'groupid' => $group['groupid']
7✔
913
                                        ]);
7✔
914
                    }
915
                } else
916
                {
917
                    $this->log->log([
4✔
918
                                        'type' => Log::TYPE_MESSAGE,
4✔
919
                                        'subtype' => Log::SUBTYPE_CLASSIFIED_SPAM,
4✔
920
                                        'msgid' => $this->msg->getID(),
4✔
921
                                        'text' => "{$rc[2]}"
4✔
922
                                    ]);
4✔
923
                }
924

925
                if ($log) { error_log("Classified as spam {$rc[2]}"); }
11✔
926
                $ret = MailRouter::FAILURE;
11✔
927

928
                if ($this->markAsSpam($rc[1], $rc[2]))
11✔
929
                {
930
                    $groups = $this->msg->getGroups(false, false);
11✔
931

932
                    if (count($groups) > 0)
11✔
933
                    {
934
                        foreach ($groups as $group)
7✔
935
                        {
936
                            $uid = $this->msg->getFromuser();
7✔
937
                            $u = User::get($this->dbhr, $this->dbhm, $uid);
7✔
938

939
                            if ($u->isBanned($group['groupid']))
7✔
940
                            {
941
                                // If they are banned we just want to drop it.
942
                                if ($log) { error_log("Banned - drop"); }
1✔
943
                                $ret = MailRouter::DROPPED;
1✔
944
                            }
945
                        }
946
                    }
947

948
                    if ($ret != MailRouter::DROPPED)
11✔
949
                    {
950
                        $ret = MailRouter::INCOMING_SPAM;
10✔
951
                        $spamfound = true;
11✔
952
                    }
953
                }
954
            } else {
955
                if ($contentcheck)
162✔
956
                {
957
                    # Now check if we think this is spam according to SpamAssassin.
958
                    #
959
                    # Need to cope with SpamAssassin being unavailable.
960
                    $this->spamc->command = 'CHECK';
68✔
961
                    $spamret = true;
68✔
962
                    $spamscore = 0;
68✔
963

964
                    try
965
                    {
966
                        $spamret = $this->spamc->filter($this->msg->getMessage());
68✔
967
                        $spamscore = $this->spamc->result['SCORE'];
68✔
968
                        if ($log) { error_log("Spam score $spamscore"); }
68✔
969
                    } catch (\Exception $e) {}
×
970

971
                    if ($spamret)
68✔
972
                    {
973
                        if ($spamscore >= MailRouter::ASSASSIN_THRESHOLD && ($this->msg->getEnvelopefrom(
68✔
974
                                ) != 'from@test.com'))
68✔
975
                        {
976
                            # This might be spam.  We'll mark it as such, then it will get reviewed.
977
                            #
978
                            # Hacky if test to stop our UT messages getting flagged as spam unless we want them to be.
979
                            $groups = $this->msg->getGroups(false, false);
10✔
980

981
                            if (count($groups) > 0)
10✔
982
                            {
983
                                foreach ($groups as $group)
9✔
984
                                {
985
                                    $this->log->log([
9✔
986
                                                        'type' => Log::TYPE_MESSAGE,
9✔
987
                                                        'subtype' => Log::SUBTYPE_CLASSIFIED_SPAM,
9✔
988
                                                        'msgid' => $this->msg->getID(),
9✔
989
                                                        'text' => "SpamAssassin score $spamscore",
9✔
990
                                                        'groupid' => $group['groupid']
9✔
991
                                                    ]);
9✔
992
                                }
993
                            } else
994
                            {
995
                                $this->log->log([
1✔
996
                                                    'type' => Log::TYPE_MESSAGE,
1✔
997
                                                    'subtype' => Log::SUBTYPE_CLASSIFIED_SPAM,
1✔
998
                                                    'msgid' => $this->msg->getID(),
1✔
999
                                                    'text' => "SpamAssassin score $spamscore"
1✔
1000
                                                ]);
1✔
1001
                            }
1002

1003
                            if ($this->markAsSpam(
10✔
1004
                                Spam::REASON_SPAMASSASSIN,
10✔
1005
                                "SpamAssassin flagged this as possible spam; score $spamscore (high is bad)"
10✔
1006
                            ))
10✔
1007
                            {
1008
                                $ret = MailRouter::INCOMING_SPAM;
9✔
1009
                                $spamfound = true;
9✔
1010
                            } else
1011
                            {
1012
                                error_log("Failed to mark as spam");
1✔
1013
                                $this->msg->recordFailure('Failed to mark spam');
1✔
1014
                                $ret = MailRouter::FAILURE;
68✔
1015
                            }
1016
                        }
1017
                    } else
1018
                    {
1019
                        # We have failed to check that this is spam.  Record the failure but carry on.
1020
                        error_log("Failed to check spam " . $this->spamc->err);
2✔
1021
                        $this->msg->recordFailure('Spam Assassin check failed ' . $this->spamc->err);
2✔
1022
                    }
1023
                }
1024
            }
1025
        }
1026

1027
        return [ $spamscore, $spamfound, $groups, $notspam, $ret ];
167✔
1028
    }
1029

1030
    private function toGroup(bool $log, $notspam, $groups, $address, $to)
1031
    {
1032
        # We're expecting to do something with this.
1033
        $envto = $this->msg->getEnvelopeto();
144✔
1034
        if ($log) { error_log("To a group; to user $envto source " . $this->msg->getSource()); }
144✔
1035
        $ret = MailRouter::FAILURE;
144✔
1036
        $source = $this->msg->getSource();
144✔
1037

1038
        if ($notspam && $source == Message::PLATFORM)
144✔
1039
        {
1040
            # It should go into pending on here.
1041
            if ($log) { error_log("Mark as pending"); }
×
1042

1043
            if ($this->markPending($notspam))
×
1044
            {
1045
                $ret = MailRouter::PENDING;
×
1046
            }
1047
        } else
1048
        {
1049
            if ($this->msg->getSource() == Message::EMAIL)
144✔
1050
            {
1051
                $uid = $this->msg->getFromuser();
144✔
1052
                if ($log)
144✔
1053
                {
1054
                    error_log("Email source, user $uid");
144✔
1055
                }
1056

1057
                if ($uid)
144✔
1058
                {
1059
                    $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
144✔
1060
                    $u = User::get($this->dbhr, $this->dbhm, $uid);
144✔
1061

1062
                    $tmps = [
144✔
1063
                        $this->msg->getID() => [
144✔
1064
                            'id' => $this->msg->getID()
144✔
1065
                        ]
144✔
1066
                    ];
144✔
1067

1068
                    $relateds = [];
144✔
1069
                    $this->msg->getPublicRelated($relateds, $tmps);
144✔
1070

1071
                    if (($this->msg->getType() == Message::TYPE_TAKEN || $this->msg->getType(
144✔
1072
                            ) == Message::TYPE_RECEIVED) &&
144✔
1073
                        count($relateds[$this->msg->getID()]))
144✔
1074
                    {
1075
                        # This is a TAKEN/RECEIVED which has been paired to an original message.  No point
1076
                        # showing it to the mods, as all they should do is approve it.
1077
                        if ($log) { error_log("TAKEN/RECEIVED paired, no need to show"); }
3✔
1078
                        $ret = MailRouter::TO_SYSTEM;
3✔
1079
                    } else
1080
                    {
1081
                        # Drop unless the email comes from a group member.
1082
                        $ret = MailRouter::DROPPED;
144✔
1083

1084
                        # Check the message for worry words.
1085
                        foreach ($groups as $group) {
144✔
1086
                            $w = new WorryWords($this->dbhr, $this->dbhm, $group['groupid']);
144✔
1087
                            $worry = $w->checkMessage(
144✔
1088
                                $this->msg->getID(),
144✔
1089
                                $this->msg->getFromuser(),
144✔
1090
                                $this->msg->getSubject(),
144✔
1091
                                $this->msg->getTextbody()
144✔
1092
                            );
144✔
1093

1094
                            $appmemb = $u->isApprovedMember($group['groupid']);
144✔
1095
                            $ourPS = $u->getMembershipAtt($group['groupid'], 'ourPostingStatus');
144✔
1096

1097
                            if ($ourPS == Group::POSTING_PROHIBITED) {
144✔
1098
                                if ($log) {
1✔
1099
                                    error_log("Prohibited, drop");
1✔
1100
                                }
1101
                                $ret = MailRouter::DROPPED;
1✔
1102
                            } else if (!getenv('NO_UNMAPPED_TO_PENDING') && !$this->msg->getPrivate('lat') && !$this->msg->getPrivate('lng')) {
143✔
1103
                                # The env variable is used in the tests to avoid this code path.
1104
                                if ($log) { error_log("Not mapped, route to Pending"); }
×
1105
                                $ret = MailRouter::DROPPED;
×
1106
                                if ($this->markPending($notspam)) {
×
1107
                                    $ret = MailRouter::PENDING;
×
1108
                                }
1109
                            } else if (!$notspam && $appmemb && $worry)
143✔
1110
                            {
1111
                                if ($log) { error_log("Worrying => pending"); }
4✔
1112
                                if ($this->markPending($notspam))
4✔
1113
                                {
1114
                                    $ret = MailRouter::PENDING;
4✔
1115
                                    $this->markAsSpam(Spam::REASON_WORRY_WORD, 'Referred to worry word');
4✔
1116
                                }
1117
                            } else {
1118
                                if ($log) { error_log("Approved member " . $u->getEmailPreferred() . " on {$group['groupid']}? $appmemb"); }
139✔
1119

1120
                                if ($appmemb)
139✔
1121
                                {
1122
                                    # Otherwise whether we post to pending or approved depends on the group setting,
1123
                                    # and if that is set not to moderate, the user setting.  Similar code for
1124
                                    # this setting in message API call.
1125
                                    #
1126
                                    # For posts by email we moderate all posts by moderators, to avoid accidents -
1127
                                    # this has been requested by volunteers.
1128
                                    $g = Group::get($this->dbhr, $this->dbhm, $group['groupid']);
137✔
1129

1130
                                    if ($log) { error_log("Check big switch " . $g->getPrivate('overridemoderation'));
137✔
1131
                                    }
1132
                                    if ($g->getPrivate('overridemoderation') == Group::OVERRIDE_MODERATION_ALL)
137✔
1133
                                    {
1134
                                        # The Big Switch is in operation.
1135
                                        $ps = Group::POSTING_MODERATED;
1✔
1136
                                    } else
1137
                                    {
1138
                                        $groupModerated = $g->getSetting(
136✔
1139
                                            'moderated',
136✔
1140
                                            0
136✔
1141
                                        );
136✔
1142

1143
                                        $ps = ($u->isModOrOwner($group['groupid']) || $groupModerated) ? Group::POSTING_MODERATED : $ourPS;
136✔
1144
                                        $ps = $ps ? $ps : Group::POSTING_MODERATED;
136✔
1145
                                        if ($log)
136✔
1146
                                        {
1147
                                            error_log("Member of {$group['groupid']}, Our PS is $ps, group moderated? " . ($groupModerated ? 'yes': 'no'));
136✔
1148
                                        }
1149
                                    }
1150

1151
                                    if ($ps == Group::POSTING_MODERATED)
137✔
1152
                                    {
1153
                                        if ($log) { error_log("Mark as pending"); }
33✔
1154

1155
                                        if ($this->markPending($notspam))
33✔
1156
                                        {
1157
                                            $ret = MailRouter::PENDING;
33✔
1158
                                        }
1159
                                    } else
1160
                                    {
1161
                                        if ($log) { error_log("Mark as approved"); }
105✔
1162
                                        $ret = MailRouter::FAILURE;
105✔
1163

1164
                                        if ($this->markApproved())
105✔
1165
                                        {
1166
                                            $ret = MailRouter::APPROVED;
105✔
1167
                                        }
1168
                                    }
1169

1170
                                    # Record the posting of this message.
1171
                                    $sql = "INSERT INTO messages_postings (msgid, groupid, repost, autorepost) VALUES(?,?,?,?);";
137✔
1172
                                    $this->dbhm->preExec($sql, [
137✔
1173
                                        $this->msg->getId(),
137✔
1174
                                        $g->getId(),
137✔
1175
                                        0,
137✔
1176
                                        0
137✔
1177
                                    ]);
137✔
1178
                                } else {
1179
                                    # Not a member.  Reply to let them know.  This is particularly useful to
1180
                                    # Trash Nothing.
1181
                                    #
1182
                                    # This isn't a pretty mail, but it's not a very common case at all.
1183
                                    $this->mail(
4✔
1184
                                        $address,
4✔
1185
                                        $to,
4✔
1186
                                        "Message Rejected",
4✔
1187
                                        "You posted by email to $to, but you're not a member of that group.",
4✔
1188
                                        Mail::NOT_A_MEMBER,
4✔
1189
                                        $uid
4✔
1190
                                    );
4✔
1191
                                    $ret = MailRouter::DROPPED;
4✔
1192
                                }
1193
                            }
1194
                        }
1195

1196
                        if ($ret == MailRouter::DROPPED) {
144✔
1197
                            if ($log) { error_log("Not a member - drop it"); }
5✔
1198
                        }
1199
                    }
1200
                }
1201
            }
1202
        }
1203

1204
        return $ret;
144✔
1205
    }
1206

1207
    private function replyToSingleMessage($matches, bool $log, $ret, $spamfound)
1208
    {
1209
        if (!$this->msg->isBounce() && !$this->msg->isAutoreply())
4✔
1210
        {
1211
            $msgid = intval($matches[1]);
4✔
1212
            $fromid = intval($matches[2]);
4✔
1213

1214
            $m = new Message($this->dbhr, $this->dbhm, $msgid);
4✔
1215
            $groups = $m->getGroups(false, true);
4✔
1216
            $closed = false;
4✔
1217
            foreach ($groups as $gid) {
4✔
1218
                $g = Group::get($this->dbhr, $this->dbhm, $gid);
3✔
1219

1220
                if ($g->getSetting('closed', false))
3✔
1221
                {
1222
                    $closed = true;
1✔
1223
                }
1224
            }
1225

1226
            if ($closed)
4✔
1227
            {
1228
                if ($log)
1✔
1229
                {
1230
                    error_log("Reply to message on closed group");
1✔
1231
                }
1232
                $this->mail(
1✔
1233
                    $this->msg->getFromaddr(),
1✔
1234
                    NOREPLY_ADDR,
1✔
1235
                    "This community is currently closed",
1✔
1236
                    "This Freegle community is currently closed.\r\n\r\nThis is an automated message - please do not reply.",
1✔
1237
                    Mail::MODMAIL,
1✔
1238
                    $fromid
1✔
1239
                );
1✔
1240
                $ret = MailRouter::TO_SYSTEM;
1✔
1241
            } else {
1242
                # Find the latest entry in messages_history for this message.
1243
                $hist = $this->dbhr->preQuery("SELECT DATEDIFF(NOW(), arrival) AS daysago FROM messages_history WHERE msgid = ? ORDER BY id DESC LIMIT 1;", [
3✔
1244
                    $msgid
3✔
1245
                ]);
3✔
1246

1247
                $daysago = count($hist) ? $hist[0]['daysago'] : (time() - strtotime($m->getPrivate('arrival'))) / 86400;
3✔
1248

1249
                if (!$m->getId() || $daysago > Message::EXPIRE_TIME) {
3✔
1250
                    # This is a reply to a message which shouldn't still be getting replies.  This can happen if
1251
                    # a mailbox is hacked and old email addresses get on a spammer mailing list.
1252
                    if ($log) { error_log("Reply to expired message, $daysago days old, reply subject " . $this->msg->getSubject()); }
1✔
1253
                    $ret = MailRouter::DROPPED;
1✔
1254
                } else {
1255
                    $u = User::get($this->dbhr, $this->dbhm, $fromid);
2✔
1256
                    $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $fromid;");
2✔
1257

1258
                    if ($m->getID() && $u->getId() && $m->getFromuser())
2✔
1259
                    {
1260
                        # The email address that we replied from might not currently be attached to the
1261
                        # other user, for example if someone has email forwarding set up.  So make sure we
1262
                        # have it.
1263
                        $u->addEmail($this->msg->getEnvelopefrom(), 0, false);
2✔
1264

1265
                        # The sender of this reply will always be on our platform, because otherwise we
1266
                        # wouldn't have generated a What's New mail to them.  So we want to set up a chat
1267
                        # between them and the sender of the message (who might or might not be on our
1268
                        # platform).
1269
                        if ($log)
2✔
1270
                        {
1271
                            error_log(
2✔
1272
                                "Create chat between " . $this->msg->getFromuser() . " (" . $this->msg->getFromaddr(
2✔
1273
                                ) . ") and $fromid for $msgid"
2✔
1274
                            );
2✔
1275
                        }
1276
                        $r = new ChatRoom($this->dbhr, $this->dbhm);
2✔
1277

1278
                        if ($fromid != $m->getFromuser()) {
2✔
1279
                            list ($chatid, $blocked) = $r->createConversation($fromid, $m->getFromuser());
2✔
1280

1281
                            # Now add this into the conversation as a message.  This will notify them.
1282
                            $textbody = $this->msg->stripQuoted();
2✔
1283

1284
                            if (strlen($textbody))
2✔
1285
                            {
1286
                                # Sometimes people will just email the photos, with no message.  We don't want to
1287
                                # create a blank chat message in that case, and such a message would get held
1288
                                # for review anyway.
1289
                                $cm = new ChatMessage($this->dbhr, $this->dbhm);
2✔
1290
                                list ($mid, $banned) = $cm->create(
2✔
1291
                                    $chatid,
2✔
1292
                                    $fromid,
2✔
1293
                                    $textbody,
2✔
1294
                                    ChatMessage::TYPE_INTERESTED,
2✔
1295
                                    $msgid,
2✔
1296
                                    false,
2✔
1297
                                    null,
2✔
1298
                                    null,
2✔
1299
                                    null,
2✔
1300
                                    null,
2✔
1301
                                    null,
2✔
1302
                                    $spamfound
2✔
1303
                                );
2✔
1304

1305
                                if ($mid)
2✔
1306
                                {
1307
                                    $cm->chatByEmail($mid, $this->msg->getID());
2✔
1308
                                }
1309
                            }
1310

1311
                            # Add any photos.
1312
                            $this->addPhotosToChat($chatid);
2✔
1313

1314
                            if ($m->hasOutcome())
2✔
1315
                            {
1316
                                # We don't want to email the recipient - no point pestering them with more
1317
                                # emails for items which are completed.  They can see them on the
1318
                                # site if they want.
1319
                                if ($log)
×
1320
                                {
1321
                                    error_log("Don't mail as promised to someone else $mid");
×
1322
                                }
1323
                                $r->mailedLastForUser($m->getFromuser());
×
1324
                            }
1325

1326
                            $ret = MailRouter::TO_USER;
2✔
1327
                        } else {
1328
                            if ($log) { error_log("Email reply to self"); }
×
1329
                            $ret = MailRouter::DROPPED;
×
1330
                        }
1331
                    }
1332
                }
1333
            }
1334
        }
1335
        return $ret;
4✔
1336
    }
1337

1338
    private function replyToChatNotification($matches, bool $log, $ret, $spamfound)
1339
    {
1340
        # It's a reply to an email notification.
1341
        $chatid = intval($matches[1]);
7✔
1342
        $userid = intval($matches[2]);
7✔
1343

1344
        $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid);
7✔
1345
        $u = User::get($this->dbhr, $this->dbhm, $userid);
7✔
1346

1347
        # We want to filter out autoreplies.  But occasionally a genuine message can contain auto
1348
        # reply text.  Most autoreplies will happen rapidly, so don't count it as an autoreply if
1349
        # it is a bit later.  This avoids us dropped genuine messages.
1350
        $latestmessage = $r->getPrivate('latestmessage');
7✔
1351
        $recentmessage = $latestmessage && (time() - strtotime($latestmessage) < 5 * 60 * 60);
7✔
1352

1353
        if ($this->msg->isReceipt())
7✔
1354
        {
1355
            # This is a read receipt which has been sent to the wrong place by a silly email client.
1356
            # Just drop these.
1357
            if ($log) { error_log("Misdirected read receipt drop"); }
×
1358
            $ret = MailRouter::DROPPED;
×
1359
        } else
1360
        {
1361
            if (!$this->msg->isBounce() && (!$recentmessage || !$this->msg->isAutoreply()))
7✔
1362
            {
1363
                # Bounces shouldn't get through - might reveal info.
1364
                #
1365
                # Auto-replies shouldn't get through.  They're used by spammers, and generally the
1366
                # content isn't very relevant in our case, e.g. if you're not in the office.
1367
                $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $userid;");
7✔
1368
                if ($r->getId())
7✔
1369
                {
1370
                    # It's a valid chat.
1371
                    if ($r->getPrivate('user1') == $userid || $r->getPrivate('user2') == $userid || $u->isModerator())
7✔
1372
                    {
1373
                        # ...and the user we're replying to is part of it or a mod.
1374
                        #
1375
                        # Check if the email address that we replied from is currently attached to the other user.
1376
                        $emails = $u->getEmails();
7✔
1377
                        $found = FALSE;
7✔
1378

1379
                        foreach ($emails as $e) {
7✔
1380
                            if ($e['email'] == $this->msg->getEnvelopefrom()) {
7✔
1381
                                $found = TRUE;
3✔
1382
                                break;
3✔
1383
                            }
1384
                        }
1385

1386
                        if (!$found) {
7✔
1387
                            # The email address that we replied from might not currently be attached to the
1388
                            # other user, for example if someone has email forwarding set up.  In that case we'd want
1389
                            # to add it.
1390
                            #
1391
                            # But if mailboxes are hacked, then the notify- email might have been harvested and we
1392
                            # might get spammers mailing that address.  There's no really good way to spot this,
1393
                            # but if we get a mail from an unassociated email address to a chat which hasn't been
1394
                            # active for a while, then that's quite likely to be spam.
1395
                            $lastmessage = $r->getPrivate('latestmessage');
5✔
1396

1397
                            if ($lastmessage && (time() - strtotime($lastmessage) > User::OPEN_AGE * 24 * 60 * 60)) {
5✔
1398
                                # It's been a while since the last message, so this is probably spam.
1399
                                if ($log) { error_log("Spammy reply to chat $chatid from $userid"); }
1✔
1400
                                $ret = MailRouter::DROPPED;
1✔
1401
                            } else {
1402
                                # It's probably not spam, so add the email address.
1403
                                $u->addEmail($this->msg->getEnvelopefrom(), 0, false);
4✔
1404
                                $found = TRUE;
4✔
1405
                            }
1406
                        }
1407

1408
                        if ($found) {
7✔
1409
                            # Now add this into the conversation as a message.  This will notify them.
1410
                            $textbody = $this->msg->stripQuoted();
6✔
1411

1412
                            if (strlen($textbody))
6✔
1413
                            {
1414
                                # Sometimes people will just email the photos, with no message.  We don't want to
1415
                                # create a blank chat message in that case, and such a message would get held
1416
                                # for review anyway.
1417
                                $cm = new ChatMessage($this->dbhr, $this->dbhm);
6✔
1418
                                list ($mid, $banned) = $cm->create(
6✔
1419
                                    $chatid,
6✔
1420
                                    $userid,
6✔
1421
                                    $textbody,
6✔
1422
                                    ChatMessage::TYPE_DEFAULT,
6✔
1423
                                    null,
6✔
1424
                                    false,
6✔
1425
                                    null,
6✔
1426
                                    null,
6✔
1427
                                    null,
6✔
1428
                                    null,
6✔
1429
                                    null,
6✔
1430
                                    $spamfound
6✔
1431
                                );
6✔
1432

1433
                                if ($mid)
6✔
1434
                                {
1435
                                    $cm->chatByEmail($mid, $this->msg->getID());
6✔
1436
                                }
1437
                            }
1438

1439
                            # Add any photos.
1440
                            $this->addPhotosToChat($chatid);
6✔
1441

1442
                            # It might be nice to suppress email notifications if the message has already
1443
                            # been promised or is complete, but we don't really know which message this
1444
                            # reply is for.
1445

1446
                            $ret = MailRouter::TO_USER;
7✔
1447
                        }
1448
                    }
1449
                }
1450
            } else
1451
            {
1452
                if ($log)
×
1453
                {
1454
                    error_log("Bounce " . $this->msg->isBounce() . " auto " . $this->msg->isAutoreply());
×
1455
                }
1456
            }
1457
        }
1458
        return $ret;
7✔
1459
    }
1460

1461
    private function directMailToUser($u, $to, bool $log, $spamscore, $spamfound)
1462
    {
1463
        # See if it's a direct reply.  Auto-replies (that we can identify) we just drop.
1464
        $uid = $u->findByEmail($to);
21✔
1465
        if ($log)
21✔
1466
        {
1467
            error_log("Find direct reply from $to = user # $uid");
21✔
1468
        }
1469

1470
        if ($uid && $this->msg->getFromuser() && strtolower($to) != strtolower(MODERATOR_EMAIL))
21✔
1471
        {
1472
            # This is to one of our users.  We try to pair it as best we can with one of the posts.
1473
            #
1474
            # We don't want to process replies to ModTools user.  This can happen if MT is a member
1475
            # rather than a mod on a group.
1476
            $this->dbhm->background(
17✔
1477
                "UPDATE users SET lastaccess = NOW() WHERE id = " . $this->msg->getFromuser() . ";"
17✔
1478
            );
17✔
1479
            $original = $this->msg->findFromReply($uid);
17✔
1480
            if ($log)
17✔
1481
            {
1482
                error_log("Paired with $original");
17✔
1483
            }
1484

1485
            $ret = MailRouter::TO_USER;
17✔
1486

1487
            $textbody = $this->msg->stripQuoted();
17✔
1488

1489
            # If we found a message to pair it with, then we will pass that as a referenced
1490
            # message.  If not then add in the subject line as that might shed some light on it.
1491
            $textbody = $original ? $textbody : ($this->msg->getSubject() . "\r\n\r\n$textbody");
17✔
1492

1493
            # Get/create the chat room between the two users.
1494
            if ($log)
17✔
1495
            {
1496
                error_log(
17✔
1497
                    "Create chat between " . $this->msg->getFromuser() . " (" . $this->msg->getFromaddr(
17✔
1498
                    ) . ") and $uid ($to)"
17✔
1499
                );
17✔
1500
            }
1501
            $r = new ChatRoom($this->dbhr, $this->dbhm);
17✔
1502
            list ($rid, $blocked) = $r->createConversation($this->msg->getFromuser(), $uid);
17✔
1503
            if ($log)
17✔
1504
            {
1505
                error_log("Got chat id $rid");
17✔
1506
            }
1507

1508
            if ($rid)
17✔
1509
            {
1510
                # Add in a spam score for the message.
1511
                if (!$spamscore)
17✔
1512
                {
1513
                    $this->spamc->command = 'CHECK';
16✔
1514
                    if ($this->spamc->filter($this->msg->getMessage()))
16✔
1515
                    {
1516
                        $spamscore = $this->spamc->result['SCORE'];
16✔
1517
                        if ($log)
16✔
1518
                        {
1519
                            error_log("Spam score $spamscore");
16✔
1520
                        }
1521
                    }
1522
                }
1523

1524
                # And now add our text into the chat room as a message.  This will notify them.
1525
                $m = new ChatMessage($this->dbhr, $this->dbhm);
17✔
1526
                list ($mid, $banned) = $m->create(
17✔
1527
                    $rid,
17✔
1528
                    $this->msg->getFromuser(),
17✔
1529
                    $textbody,
17✔
1530
                    $this->msg->getModmail() ? ChatMessage::TYPE_MODMAIL : ChatMessage::TYPE_INTERESTED,
17✔
1531
                    $original,
17✔
1532
                    false,
17✔
1533
                    $spamscore,
17✔
1534
                    null,
17✔
1535
                    null,
17✔
1536
                    null,
17✔
1537
                    null,
17✔
1538
                    $spamfound
17✔
1539
                );
17✔
1540
                if ($log)
17✔
1541
                {
1542
                    error_log("Created chat message $mid");
17✔
1543
                }
1544

1545
                $m->chatByEmail($mid, $this->msg->getID());
17✔
1546

1547
                # Add any photos.
1548
                $this->addPhotosToChat($rid);
17✔
1549

1550
                if ($original)
17✔
1551
                {
1552
                    $m = new Message($this->dbhr, $this->dbhm, $original);
15✔
1553

1554
                    if ($m->hasOutcome())
15✔
1555
                    {
1556
                        # We don't want to email the recipient - no point pestering them with more
1557
                        # emails for items which are completed.  They can see them on the
1558
                        # site if they want.
1559
                        if ($log)
×
1560
                        {
1561
                            error_log("Don't mail as promised to someone else $mid");
×
1562
                        }
1563
                        $r->mailedLastForUser($m->getFromuser());
17✔
1564
                    }
1565
                }
1566
            }
1567
        } else {
1568
            if ($log) { error_log("Not to group and not reply - drop"); }
4✔
1569
            $ret = MailRouter::DROPPED;
4✔
1570
        }
1571

1572
        return $ret;
21✔
1573
    }
1574

1575
    public function setLatLng($lat, $lng) {
1576
        $this->msg->setPrivate('lat', $lat);
16✔
1577
        $this->msg->setPrivate('lng', $lng);
16✔
1578
    }
1579
}
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

© 2026 Coveralls, Inc