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

Freegle / iznik-server / 9785cc63-4b10-470b-b841-69247e465a29

20 Dec 2024 04:18PM UTC coverage: 92.423% (-0.003%) from 92.426%
9785cc63-4b10-470b-b841-69247e465a29

push

circleci

edwh
Don't use microvolunteering to review autoreposts.

25492 of 27582 relevant lines covered (92.42%)

31.47 hits per line

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

93.04
/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;
186✔
55
        $this->dbhm = $dbhm;
186✔
56
        $this->log = new Log($this->dbhr, $this->dbhm);
186✔
57
        $this->spamc = new spamc;
186✔
58
        $this->spam = new Spam($this->dbhr, $this->dbhm);
186✔
59

60
        if ($id) {
186✔
61
            $this->msg = new Message($this->dbhr, $this->dbhm, $id);
13✔
62
        } else {
63
            $this->msg = new Message($this->dbhr, $this->dbhm);
174✔
64
        }
65
    }
66

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

77
        if ($rc) {
171✔
78
            $ret = $this->msg->save($log);
171✔
79
        }
80
        
81
        return($ret);
171✔
82
    }
83

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

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

106
        # Now visible in search
107
        $this->msg->index();
105✔
108

109
        return($rc);
105✔
110
    }
111

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

125
        if (!$force) {
36✔
126
            $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() ]);
36✔
127
            $overq = count($groups) == 0 ? " AND collection = 'Incoming' " : '';
36✔
128
            #error_log("MarkPending " . $this->msg->getID() . " from collection $overq");
129
        }
130

131
        $rc = $this->dbhm->preExec("UPDATE messages_groups SET collection = 'Pending' WHERE msgid = ? $overq;", [
36✔
132
            $this->msg->getID()
36✔
133
        ]);
36✔
134

135
        # Notify mods of new work
136
        $groups = $this->msg->getGroups();
36✔
137
        $n = new PushNotifications($this->dbhr, $this->dbhm);
36✔
138

139
        foreach ($groups as $groupid) {
36✔
140
            $n->notifyGroupMods($groupid);
36✔
141
            error_log("Pending notify $groupid");
36✔
142
        }
143

144
        return($rc);
36✔
145
    }
146

147
    public function route($msg = NULL, $notspam = FALSE) {
148
        $ret = NULL;
181✔
149
        $log = TRUE;
181✔
150
        $keepgroups = FALSE;
181✔
151

152
        # We route messages to one of the following destinations:
153
        # - to a handler for system messages
154
        #   - confirmation of Yahoo mod status
155
        #   - confirmation of Yahoo subscription requests
156
        # - to a group, either pending or approved
157
        # - to group moderators
158
        # - to a user
159
        # - to a spam queue
160
        if ($msg) {
181✔
161
            $this->msg = $msg;
11✔
162
        }
163

164
        if ($notspam) {
181✔
165
            # Record that this message has been flagged as not spam.
166
            if ($this->log) { error_log("Record message as not spam"); }
1✔
167
            $this->msg->setPrivate('spamtype', Spam::REASON_NOT_SPAM, TRUE);
1✔
168
        }
169

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

176
        $to = $this->msg->getEnvelopeto();
181✔
177
        $from = $this->msg->getEnvelopefrom();
181✔
178
        $fromheader = $this->msg->getHeader('from');
181✔
179

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

184
        if ($fromheader) {
181✔
185
            $fromheader = mailparse_rfc822_parse_addresses($fromheader);
181✔
186
        }
187

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

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

239
            if (!$ret) {
166✔
240
                # Not obviously spam.
241
                if ($log) { error_log("Not obviously spam, groups " . var_export($groups, TRUE)); }
154✔
242

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

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

275
        if ($ret != MailRouter::FAILURE && !$keepgroups) {
181✔
276
            # Ensure no message is stuck in incoming.
277
            $this->dbhm->preExec("DELETE FROM messages_groups WHERE msgid = ? AND collection = ?;", [
181✔
278
                $this->msg->getID(),
181✔
279
                MessageCollection::INCOMING
181✔
280
            ]);
181✔
281
        }
282

283
        $this->dbhm->preExec("UPDATE messages SET lastroute = ? WHERE id = ?;", [
181✔
284
            $ret,
181✔
285
            $this->msg->getID()
181✔
286
        ]);
181✔
287

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

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

307
        return($ret);
181✔
308
    }
309

310
    private function addPhotosToChat($rid) {
311
        $m = new ChatMessage($this->dbhr, $this->dbhm);
29✔
312
        $count = 0;
29✔
313

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

319
        foreach ($atts as $att) {
29✔
320
            $hash = $att->getHash();
2✔
321

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

327
                if ($mid) {
2✔
328
                    $a = new Attachment($this->dbhr, $this->dbhm, NULL, Attachment::TYPE_CHAT_MESSAGE);
2✔
329
                    list ($aid2, $uid) = $a->create($mid, NULL, $att->getExternalUid(), $att->getExternalUrl(), TRUE, $att->getExternalMods(), $att->getHash());
2✔
330
                    $m->setPrivate('imageid', $aid2);
2✔
331
                    $att->delete();
2✔
332

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

343
                    if ($used[0]['count'] > Spam::IMAGE_THRESHOLD) {
2✔
344
                        $m->setPrivate('reviewrequired', 1);
1✔
345
                        $m->setPrivate('reportreason', Spam::REASON_IMAGE_SENT_MANY_TIMES);
1✔
346
                    }
347

348
                    $count++;
2✔
349
                }
350
            }
351
        }
352

353
        return($count);
29✔
354
    }
355

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

365
                if (!$msg->getDeleted()) {
1✔
366
                    $this->route($msg);
1✔
367
                }
368
            } catch (\Exception $e) {
1✔
369
                # Ignore this and continue routing the rest.
370
                error_log("Route #" . $this->msg->getID() . " failed " . $e->getMessage() . " stack " . $e->getTraceAsString());
1✔
371
                if ($this->dbhm->inTransaction()) {
1✔
372
                    $this->dbhm->rollBack();
×
373
                }
374
            }
375
        }
376
    }
377

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

382
        $message = \Swift_Message::newInstance()
5✔
383
            ->setSubject($subject)
5✔
384
            ->setFrom($from)
5✔
385
            ->setTo($to)
5✔
386
            ->setBody($body);
5✔
387

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

390
        $mailer->send($message);
5✔
391
    }
392

393
    private function FBL() {
394
        if ($this->log) { error_log("FBL report"); }
1✔
395

396
        // Find the email address that the FBL was about, in a hacky regex way.
397
        $handled = FALSE;
1✔
398
        $msg = $this->msg->getMessage();
1✔
399

400
        if (preg_match('/Original-Rcpt-To:(.*)/', $msg, $matches)) {
1✔
401
            $email = trim($matches[1]);
1✔
402
            if ($this->log) { error_log("FBL report to $email"); }
1✔
403

404
            $u = new User($this->dbhr, $this->dbhm);
1✔
405
            $uid = $u->findByEmail($email);
1✔
406
            if ($this->log) { error_log("FBL report for $uid"); }
1✔
407

408
            if ($uid) {
1✔
409
                $u = User::get($this->dbhr, $this->dbhm, $uid);
1✔
410
                $u->setSimpleMail(User::SIMPLE_MAIL_NONE);
1✔
411
                $u->FBL();
1✔
412
                $handled = TRUE;
1✔
413
            }
414
        }
415

416
        if (!$handled) {
1✔
417
            if ($this->log) { error_log("FBL report not processed"); }
×
418

419
            $this->mail(
×
420
                'log@ehibbert.org.uk',
×
421
                NOREPLY_ADDR,
×
422
                "Unprocessed FBL report received",
×
423
                $this->msg->getMessage(),
×
424
                Mail::MODMAIL,
×
425
                0
×
426
            );
×
427
        }
428

429
        return MailRouter::TO_SYSTEM;
1✔
430
    }
431

432
    private function turnDigestOff($matches, $ret)
433
    {
434
        # Request to turn email off.
435
        $uid = intval($matches[1]);
1✔
436
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
437
        $groupid = intval($matches[2]);
1✔
438

439
        if ($uid && $groupid)
1✔
440
        {
441
            $d = new Digest($this->dbhr, $this->dbhm);
1✔
442
            $d->off($uid, $groupid);
1✔
443

444
            $ret = MailRouter::TO_SYSTEM;
1✔
445
        }
446
        return $ret;
1✔
447
    }
448

449
    private function readReceipt($matches)
450
    {
451
        # Read receipt
452
        $chatid = intval($matches[1]);
1✔
453
        $userid = intval($matches[2]);
1✔
454
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $userid;");
1✔
455
        $msgid = intval($matches[3]);
1✔
456

457
        # The receipt has seen this message, and the message has been seen by all people in the chat (because
458
        # we only generate these for user 2 user.
459
        $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid);
1✔
460
        if ($r->canSee($userid, false))
1✔
461
        {
462
            $r->updateRoster($userid, $msgid);
1✔
463
            $r->seenByAll($msgid);
1✔
464
        }
465

466
        $ret = MailRouter::RECEIPT;
1✔
467
        return $ret;
1✔
468
    }
469

470
    private function trystResponse($matches)
471
    {
472
        # Calendar response
473
        $trystid = intval($matches[1]);
1✔
474
        $userid = intval($matches[2]);
1✔
475
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $userid;");
1✔
476

477
        # Scan for a VCALENDAR attachment.
478
        $t = new Tryst($this->dbhr, $this->dbhm, $trystid);
1✔
479
        $rsp = Tryst::OTHER;
1✔
480

481
        foreach ($this->msg->getParsedAttachments() as $att)
1✔
482
        {
483
            $ct = $att->getContentType();
1✔
484

485
            if (strcmp('text/calendar', strtolower($ct)) === 0)
1✔
486
            {
487
                # We don't do a proper parse
488
                $vcal = strtolower($att->getContent());
1✔
489
                if (strpos($vcal, 'status:confirmed') !== false || strpos($vcal, 'status:tentative') !== false)
1✔
490
                {
491
                    $rsp = Tryst::ACCEPTED;
1✔
492
                } else
493
                {
494
                    if (strpos($vcal, 'status:cancelled') !== false)
1✔
495
                    {
496
                        $rsp = Tryst::DECLINED;
×
497
                    }
498
                }
499
            }
500
        }
501

502
        if ($rsp == Tryst::OTHER)
1✔
503
        {
504
            # Maybe they didn't put the VCALENDAR in.
505
            if (stripos($this->msg->getSubject(), 'accepted') !== false)
1✔
506
            {
507
                $rsp = Tryst::ACCEPTED;
1✔
508
            } else
509
            {
510
                if (stripos($this->msg->getSubject(), 'declined') !== false)
1✔
511
                {
512
                    $rsp = Tryst::DECLINED;
1✔
513
                }
514
            }
515
        }
516

517
        $t->response($userid, $rsp);
1✔
518

519
        $ret = MailRouter::TRYST;
1✔
520
        return $ret;
1✔
521
    }
522

523
    private function turnEventsOff($matches, $ret)
524
    {
525
        # Request to turn events email off.
526
        $uid = intval($matches[1]);
1✔
527
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
528
        $groupid = intval($matches[2]);
1✔
529

530
        if ($uid && $groupid)
1✔
531
        {
532
            $d = new EventDigest($this->dbhr, $this->dbhm);
1✔
533
            $d->off($uid, $groupid);
1✔
534

535
            $ret = MailRouter::TO_SYSTEM;
1✔
536
        }
537
        return $ret;
1✔
538
    }
539

540
    private function turnNewslettersOff($matches, $ret)
541
    {
542
        # Request to turn newsletters off.
543
        $uid = intval($matches);
1✔
544
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
545

546
        if ($uid)
1✔
547
        {
548
            $d = new Newsletter($this->dbhr, $this->dbhm);
1✔
549
            $d->off($uid);
1✔
550

551
            $ret = MailRouter::TO_SYSTEM;
1✔
552
        }
553
        return $ret;
1✔
554
    }
555

556
    private function turnRelevantOff($matches, $ret)
557
    {
558
        # Request to turn "interested in" off.
559
        $uid = intval($matches);
1✔
560
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
561

562
        if ($uid)
1✔
563
        {
564
            $d = new Relevant($this->dbhr, $this->dbhm);
1✔
565
            $d->off($uid);
1✔
566

567
            $ret = MailRouter::TO_SYSTEM;
1✔
568
        }
569
        return $ret;
1✔
570
    }
571

572
    private function turnVolunteeringOff($matches, $ret)
573
    {
574
        # Request to turn volunteering email off.
575
        $uid = intval($matches[1]);
×
576
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
×
577
        $groupid = intval($matches[2]);
×
578

579
        if ($uid && $groupid)
×
580
        {
581
            $d = new VolunteeringDigest($this->dbhr, $this->dbhm);
×
582
            $d->off($uid, $groupid);
×
583

584
            $ret = MailRouter::TO_SYSTEM;
×
585
        }
586
        return $ret;
×
587
    }
588

589
    private function turnNotificationsOff($matches, $ret)
590
    {
591
        # Request to turn notification email off.
592
        $uid = intval($matches);
1✔
593
        $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
594

595
        if ($uid)
1✔
596
        {
597
            $d = new Notifications($this->dbhr, $this->dbhm);
1✔
598
            $d->off($uid);
1✔
599

600
            $ret = MailRouter::TO_SYSTEM;
1✔
601
        }
602
        return $ret;
1✔
603
    }
604

605
    private function toVolunteers($to, $matches, $notspam)
606
    {
607
        # Mail to our owner address.  First check if it's spam according to SpamAssassin.
608
        if ($this->log) {
6✔
609
            error_log("To volunteers");
6✔
610
        }
611

612
        $this->spamc->command = 'CHECK';
6✔
613

614
        $ret = MailRouter::INCOMING_SPAM;
6✔
615

616
        if ($notspam || $this->spamc->filter($this->msg->getMessage()))
6✔
617
        {
618
            $spamscore = $this->spamc->result['SCORE'];
6✔
619

620
            if ($notspam || $spamscore < MailRouter::ASSASSIN_THRESHOLD)
6✔
621
            {
622
                # Now do our own checks.
623
                if ($this->log)
6✔
624
                {
625
                    error_log("Passed SpamAssassin $spamscore");
6✔
626
                }
627
                list ($rc, $reason) = $notspam ? [ FALSE, NULL] : $this->spam->checkMessage($this->msg);
6✔
628

629
                if (!$rc)
6✔
630
                {
631
                    # Don't pass on automated mails from ADMINs - there might be loads.
632
                    if ($notspam ||
6✔
633
                        (preg_match('/(.*)-volunteers@' . GROUP_DOMAIN . '/', $to) || !$this->msg->isBounce() && !$this->msg->isAutoreply()))
6✔
634
                    {
635
                        $ret = MailRouter::FAILURE;
6✔
636

637
                        # It's not.  Find the group
638
                        $g = new Group($this->dbhr, $this->dbhm);
6✔
639
                        $sn = $matches;
6✔
640

641
                        $gid = $g->findByShortName($sn);
6✔
642
                        if ($this->log)
6✔
643
                        {
644
                            error_log("Found $gid from $sn");
6✔
645
                        }
646

647
                        if ($gid)
6✔
648
                        {
649
                            # It's one of our groups.  Find the user this is from.
650
                            $envfrom = $this->msg->getFromaddr();
6✔
651
                            $u = new User($this->dbhr, $this->dbhm);
6✔
652
                            $uid = $u->findByEmail($envfrom);
6✔
653
                            $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
6✔
654

655
                            if ($this->log)
6✔
656
                            {
657
                                error_log("Found $uid from $envfrom");
6✔
658
                            }
659

660
                            # We should always find them as Message::parse should create them
661
                            if ($uid)
6✔
662
                            {
663
                                if ($this->log)
6✔
664
                                {
665
                                    error_log("From user $uid to group $gid");
6✔
666
                                }
667
                                $s = new Spam($this->dbhr, $this->dbhm);
6✔
668

669
                                # Filter out mail to volunteers from known spammers.
670
                                $ret = MailRouter::INCOMING_SPAM;
6✔
671
                                $spammers = $s->getSpammerByUserid($uid);
6✔
672

673
                                if (!$spammers)
6✔
674
                                {
675
                                    $ret = MailRouter::DROPPED;
6✔
676

677
                                    # Don't want to pass on OOF etc.
678
                                    if ($notspam || !$this->msg->isAutoreply())
6✔
679
                                    {
680
                                        # Create/get a chat between the sender and the group mods.
681
                                        $r = new ChatRoom($this->dbhr, $this->dbhm);
6✔
682
                                        $chatid = $r->createUser2Mod($uid, $gid);
6✔
683
                                        if ($this->log)
6✔
684
                                        {
685
                                            error_log("Chatid is $chatid");
6✔
686
                                        }
687

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

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

697
                                        if (preg_match('/(.*)^\s*-----Original Message-----(\s*)/ms', $textbody, $matches)) {
6✔
698
                                            $textbody = $matches[1];
1✔
699
                                            $textbody .= "\r\n\r\n(Replied to digest)";
1✔
700
                                        }
701

702
                                        if (strlen($textbody)) {
6✔
703
                                            $m = new ChatMessage($this->dbhr, $this->dbhm);
5✔
704

705
                                            // Force to review so that we don't mail it before we've recorded that the
706
                                            // sender has seen it.
707
                                            list ($mid, $banned) = $m->create(
5✔
708
                                                $chatid,
5✔
709
                                                $uid,
5✔
710
                                                $textbody,
5✔
711
                                                ChatMessage::TYPE_DEFAULT,
5✔
712
                                                null,
5✔
713
                                                false,
5✔
714
                                                null,
5✔
715
                                                null,
5✔
716
                                                null,
5✔
717
                                                null,
5✔
718
                                                null,
5✔
719
                                                true
5✔
720
                                            );
5✔
721

722
                                            $r->updateRoster($uid, $mid);
5✔
723

724
                                            // Allow mailing to happen.
725
                                            $m->setPrivate('reviewrequired', 0);
5✔
726

727
                                            if ($this->log)
5✔
728
                                            {
729
                                                error_log("Created message $mid");
5✔
730
                                            }
731

732
                                            $m->chatByEmail($mid, $this->msg->getID());
5✔
733
                                        }
734

735
                                        # Add any photos.
736
                                        $this->addPhotosToChat($chatid);
6✔
737

738
                                        $ret = MailRouter::TO_VOLUNTEERS;
6✔
739
                                    }
740
                                }
741
                            }
742
                        }
743
                    } else {
744
                        if ($this->log) {
×
745
                            error_log("Automated reply from ADMIN - drop");
×
746
                        }
747
                        $ret = MailRouter::DROPPED;
6✔
748
                    }
749
                } else {
750
                    if ($this->log) {
3✔
751
                        error_log("Spam: " . var_export($reason, TRUE));
3✔
752
                    }
753
                }
754
            }
755
        }
756
        return $ret;
6✔
757
    }
758

759
    private function subscribe($name)
760
    {
761
        $ret = MailRouter::FAILURE;
3✔
762

763
        # Find the group
764
        $g = new Group($this->dbhr, $this->dbhm);
3✔
765
        $gid = $g->findByShortName($name);
3✔
766
        $g = new Group($this->dbhr, $this->dbhm, $gid);
3✔
767

768
        if ($gid)
3✔
769
        {
770
            # It's one of our groups.  Find the user this is from.
771
            $envfrom = $this->msg->getEnvelopeFrom();
3✔
772
            $u = new User($this->dbhr, $this->dbhm);
3✔
773
            $uid = $u->findByEmail($envfrom);
3✔
774

775
            if (!$uid)
3✔
776
            {
777
                # We don't know them yet.
778
                $uid = $u->create(
×
779
                    null,
×
780
                    null,
×
781
                    $this->msg->getFromname(),
×
782
                    "Email subscription from $envfrom to " . $g->getPrivate('nameshort')
×
783
                );
×
784
                $u->addEmail($envfrom, 0);
×
785
                $pw = $u->inventPassword();
×
786
                $u->addLogin(User::LOGIN_NATIVE, $uid, $pw);
×
787
                $u->welcome($envfrom, $pw);
×
788
            }
789

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

793
            # We should always find them as Message::parse should create them
794
            if ($u->getId())
3✔
795
            {
796
                $u->addMembership($gid, User::ROLE_MEMBER, null, MembershipCollection::APPROVED, $envfrom);
3✔
797

798
                # Remove any email logs for this message - no point wasting space on keeping those.
799
                $this->log->deleteLogsForMessage($this->msg->getID());
3✔
800
                $ret = MailRouter::TO_SYSTEM;
3✔
801
            }
802
        }
803
        return $ret;
3✔
804
    }
805

806
    private function oneClickUnsubscribe($uid, $key, $type) {
807
        $ret = MailRouter::DROPPED;
1✔
808

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

811
        # Prevent accidental unsubscription by mods.
812
        if (!$u->isModerator()) {
1✔
813
            # Validate the key to prevent spoof unsubscribes.
814
            $ukey = $u->getUserKey($uid);
1✔
815

816
            if ($key && !strcasecmp($ukey, $key)) {
1✔
817
                $u->forget("Unsubscribed from $type");
1✔
818
                $ret = MailRouter::TO_SYSTEM;
1✔
819
            }
820
        }
821

822
        return $ret;
1✔
823
    }
824

825
    private function unsubscribe($name)
826
    {
827
        $ret = MailRouter::FAILURE;
1✔
828

829
        # Find the group
830
        $g = new Group($this->dbhr, $this->dbhm);
1✔
831
        $gid = $g->findByShortName($name);
1✔
832

833
        if ($gid)
1✔
834
        {
835
            # It's one of our groups.  Find the user this is from.
836
            $envfrom = $this->msg->getEnvelopeFrom();
1✔
837
            $u = new User($this->dbhr, $this->dbhm);
1✔
838
            $uid = $u->findByEmail($envfrom);
1✔
839
            $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
1✔
840

841
            if ($uid)
1✔
842
            {
843
                $u = new User($this->dbhr, $this->dbhm, $uid);
1✔
844
                $ret = MailRouter::DROPPED;
1✔
845

846
                if (!$u->isModOrOwner($gid))
1✔
847
                {
848
                    $u->removeMembership($gid, false, false, $envfrom);
1✔
849

850
                    # Remove any email logs for this message - no point wasting space on keeping those.
851
                    $this->log->deleteLogsForMessage($this->msg->getID());
1✔
852
                    $ret = MailRouter::TO_SYSTEM;
1✔
853
                }
854
            }
855
        }
856
        return $ret;
1✔
857
    }
858

859
    private function checkSpam($log, bool $notspam, $ret): array
860
    {
861
        # We use SpamAssassin to weed out obvious spam.  We only do a content check if the message subject line is
862
        # not in the standard format.  Most generic spam isn't in that format, and some of our messages
863
        # would otherwise get flagged - so this improves overall reliability.
864
        $contentcheck = !$notspam && !preg_match('/.*?\:(.*)\(.*\)/', $this->msg->getSubject());
166✔
865
        $spamscore = null;
166✔
866
        $spamfound = false;
166✔
867

868
        $groups = $this->msg->getGroups(false, false);
166✔
869
        #error_log("Got groups " . var_export($groups, TRUE));
870

871
        # Check if the group wants us to check for spam.
872
        foreach ($groups as $group)
166✔
873
        {
874
            $g = Group::get($this->dbhr, $this->dbhm, $group['groupid']);
153✔
875
            $defs = $g->getDefaults();
153✔
876
            $spammers = $g->getSetting('spammers', $defs['spammers']);
153✔
877
            $check = array_key_exists(
153✔
878
                'messagereview',
153✔
879
                $spammers
153✔
880
            ) ? $spammers['messagereview'] : $defs['spammers']['messagereview'];
153✔
881
            $notspam = $check ? $notspam : true;
153✔
882
            #error_log("Consider spam review $notspam from $check, " . var_export($spammers, TRUE));
883
        }
884

885
        if (!$notspam)
166✔
886
        {
887
            # First check if this message is spam based on our own checks.
888
            $rc = $this->spam->checkMessage($this->msg);
165✔
889
            if ($rc)
165✔
890
            {
891
                if (count($groups) > 0)
11✔
892
                {
893
                    foreach ($groups as $group)
7✔
894
                    {
895
                        $this->log->log([
7✔
896
                                            'type' => Log::TYPE_MESSAGE,
7✔
897
                                            'subtype' => Log::SUBTYPE_CLASSIFIED_SPAM,
7✔
898
                                            'msgid' => $this->msg->getID(),
7✔
899
                                            'text' => "{$rc[2]}",
7✔
900
                                            'groupid' => $group['groupid']
7✔
901
                                        ]);
7✔
902
                    }
903
                } else
904
                {
905
                    $this->log->log([
4✔
906
                                        'type' => Log::TYPE_MESSAGE,
4✔
907
                                        'subtype' => Log::SUBTYPE_CLASSIFIED_SPAM,
4✔
908
                                        'msgid' => $this->msg->getID(),
4✔
909
                                        'text' => "{$rc[2]}"
4✔
910
                                    ]);
4✔
911
                }
912

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

916
                if ($this->markAsSpam($rc[1], $rc[2]))
11✔
917
                {
918
                    $groups = $this->msg->getGroups(false, false);
11✔
919

920
                    if (count($groups) > 0)
11✔
921
                    {
922
                        foreach ($groups as $group)
7✔
923
                        {
924
                            $uid = $this->msg->getFromuser();
7✔
925
                            $u = User::get($this->dbhr, $this->dbhm, $uid);
7✔
926

927
                            if ($u->isBanned($group['groupid']))
7✔
928
                            {
929
                                // If they are banned we just want to drop it.
930
                                if ($log) { error_log("Banned - drop"); }
1✔
931
                                $ret = MailRouter::DROPPED;
1✔
932
                            }
933
                        }
934
                    }
935

936
                    if ($ret != MailRouter::DROPPED)
11✔
937
                    {
938
                        $ret = MailRouter::INCOMING_SPAM;
10✔
939
                        $spamfound = true;
11✔
940
                    }
941
                }
942
            } else {
943
                if ($contentcheck)
161✔
944
                {
945
                    # Now check if we think this is spam according to SpamAssassin.
946
                    #
947
                    # Need to cope with SpamAssassin being unavailable.
948
                    $this->spamc->command = 'CHECK';
68✔
949
                    $spamret = true;
68✔
950
                    $spamscore = 0;
68✔
951

952
                    try
953
                    {
954
                        $spamret = $this->spamc->filter($this->msg->getMessage());
68✔
955
                        $spamscore = $this->spamc->result['SCORE'];
68✔
956
                        if ($log) { error_log("Spam score $spamscore"); }
68✔
957
                    } catch (\Exception $e) {}
×
958

959
                    if ($spamret)
68✔
960
                    {
961
                        if ($spamscore >= MailRouter::ASSASSIN_THRESHOLD && ($this->msg->getEnvelopefrom(
68✔
962
                                ) != 'from@test.com'))
68✔
963
                        {
964
                            # This might be spam.  We'll mark it as such, then it will get reviewed.
965
                            #
966
                            # Hacky if test to stop our UT messages getting flagged as spam unless we want them to be.
967
                            $groups = $this->msg->getGroups(false, false);
10✔
968

969
                            if (count($groups) > 0)
10✔
970
                            {
971
                                foreach ($groups as $group)
9✔
972
                                {
973
                                    $this->log->log([
9✔
974
                                                        'type' => Log::TYPE_MESSAGE,
9✔
975
                                                        'subtype' => Log::SUBTYPE_CLASSIFIED_SPAM,
9✔
976
                                                        'msgid' => $this->msg->getID(),
9✔
977
                                                        'text' => "SpamAssassin score $spamscore",
9✔
978
                                                        'groupid' => $group['groupid']
9✔
979
                                                    ]);
9✔
980
                                }
981
                            } else
982
                            {
983
                                $this->log->log([
1✔
984
                                                    'type' => Log::TYPE_MESSAGE,
1✔
985
                                                    'subtype' => Log::SUBTYPE_CLASSIFIED_SPAM,
1✔
986
                                                    'msgid' => $this->msg->getID(),
1✔
987
                                                    'text' => "SpamAssassin score $spamscore"
1✔
988
                                                ]);
1✔
989
                            }
990

991
                            if ($this->markAsSpam(
10✔
992
                                Spam::REASON_SPAMASSASSIN,
10✔
993
                                "SpamAssassin flagged this as possible spam; score $spamscore (high is bad)"
10✔
994
                            ))
10✔
995
                            {
996
                                $ret = MailRouter::INCOMING_SPAM;
9✔
997
                                $spamfound = true;
9✔
998
                            } else
999
                            {
1000
                                error_log("Failed to mark as spam");
1✔
1001
                                $this->msg->recordFailure('Failed to mark spam');
1✔
1002
                                $ret = MailRouter::FAILURE;
68✔
1003
                            }
1004
                        }
1005
                    } else
1006
                    {
1007
                        # We have failed to check that this is spam.  Record the failure but carry on.
1008
                        error_log("Failed to check spam " . $this->spamc->err);
2✔
1009
                        $this->msg->recordFailure('Spam Assassin check failed ' . $this->spamc->err);
2✔
1010
                    }
1011
                }
1012
            }
1013
        }
1014

1015
        return [ $spamscore, $spamfound, $groups, $notspam, $ret ];
166✔
1016
    }
1017

1018
    private function toGroup(bool $log, $notspam, $groups, $address, $to)
1019
    {
1020
        # We're expecting to do something with this.
1021
        $envto = $this->msg->getEnvelopeto();
143✔
1022
        if ($log) { error_log("To a group; to user $envto source " . $this->msg->getSource()); }
143✔
1023
        $ret = MailRouter::FAILURE;
143✔
1024
        $source = $this->msg->getSource();
143✔
1025

1026
        if ($notspam && $source == Message::PLATFORM)
143✔
1027
        {
1028
            # It should go into pending on here.
1029
            if ($log) { error_log("Mark as pending"); }
×
1030

1031
            if ($this->markPending($notspam))
×
1032
            {
1033
                $ret = MailRouter::PENDING;
×
1034
            }
1035
        } else
1036
        {
1037
            if ($this->msg->getSource() == Message::EMAIL)
143✔
1038
            {
1039
                $uid = $this->msg->getFromuser();
143✔
1040
                if ($log)
143✔
1041
                {
1042
                    error_log("Email source, user $uid");
143✔
1043
                }
1044

1045
                if ($uid)
143✔
1046
                {
1047
                    $this->dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $uid;");
143✔
1048
                    $u = User::get($this->dbhr, $this->dbhm, $uid);
143✔
1049

1050
                    $tmps = [
143✔
1051
                        $this->msg->getID() => [
143✔
1052
                            'id' => $this->msg->getID()
143✔
1053
                        ]
143✔
1054
                    ];
143✔
1055

1056
                    $relateds = [];
143✔
1057
                    $this->msg->getPublicRelated($relateds, $tmps);
143✔
1058

1059
                    if (($this->msg->getType() == Message::TYPE_TAKEN || $this->msg->getType(
143✔
1060
                            ) == Message::TYPE_RECEIVED) &&
143✔
1061
                        count($relateds[$this->msg->getID()]))
143✔
1062
                    {
1063
                        # This is a TAKEN/RECEIVED which has been paired to an original message.  No point
1064
                        # showing it to the mods, as all they should do is approve it.
1065
                        if ($log) { error_log("TAKEN/RECEIVED paired, no need to show"); }
3✔
1066
                        $ret = MailRouter::TO_SYSTEM;
3✔
1067
                    } else
1068
                    {
1069
                        # Drop unless the email comes from a group member.
1070
                        $ret = MailRouter::DROPPED;
143✔
1071

1072
                        # Check the message for worry words.
1073
                        foreach ($groups as $group) {
143✔
1074
                            $w = new WorryWords($this->dbhr, $this->dbhm, $group['groupid']);
143✔
1075
                            $worry = $w->checkMessage(
143✔
1076
                                $this->msg->getID(),
143✔
1077
                                $this->msg->getFromuser(),
143✔
1078
                                $this->msg->getSubject(),
143✔
1079
                                $this->msg->getTextbody()
143✔
1080
                            );
143✔
1081

1082
                            $appmemb = $u->isApprovedMember($group['groupid']);
143✔
1083
                            $ourPS = $u->getMembershipAtt($group['groupid'], 'ourPostingStatus');
143✔
1084

1085
                            if ($ourPS == Group::POSTING_PROHIBITED) {
143✔
1086
                                if ($log) {
1✔
1087
                                    error_log("Prohibited, drop");
1✔
1088
                                }
1089
                                $ret = MailRouter::DROPPED;
1✔
1090
                            } else if (!getenv('NO_UNMAPPED_TO_PENDING') && !$this->msg->getPrivate('lat') && !$this->msg->getPrivate('lng')) {
142✔
1091
                                # The env variable is used in the tests to avoid this code path.
1092
                                if ($log) { error_log("Not mapped, route to Pending"); }
×
1093
                                $ret = MailRouter::PENDING;
×
1094
                            } else if (!$notspam && $appmemb && $worry)
142✔
1095
                            {
1096
                                if ($log) { error_log("Worrying => pending"); }
4✔
1097
                                if ($this->markPending($notspam))
4✔
1098
                                {
1099
                                    $ret = MailRouter::PENDING;
4✔
1100
                                    $this->markAsSpam(Spam::REASON_WORRY_WORD, 'Referred to worry word');
4✔
1101
                                }
1102
                            } else {
1103
                                if ($log) { error_log("Approved member " . $u->getEmailPreferred() . " on {$group['groupid']}? $appmemb"); }
138✔
1104

1105
                                if ($appmemb)
138✔
1106
                                {
1107
                                    # Otherwise whether we post to pending or approved depends on the group setting,
1108
                                    # and if that is set not to moderate, the user setting.  Similar code for
1109
                                    # this setting in message API call.
1110
                                    #
1111
                                    # For posts by email we moderate all posts by moderators, to avoid accidents -
1112
                                    # this has been requested by volunteers.
1113
                                    $g = Group::get($this->dbhr, $this->dbhm, $group['groupid']);
136✔
1114

1115
                                    if ($log) { error_log("Check big switch " . $g->getPrivate('overridemoderation'));
136✔
1116
                                    }
1117
                                    if ($g->getPrivate('overridemoderation') == Group::OVERRIDE_MODERATION_ALL)
136✔
1118
                                    {
1119
                                        # The Big Switch is in operation.
1120
                                        $ps = Group::POSTING_MODERATED;
1✔
1121
                                    } else
1122
                                    {
1123
                                        $groupModerated = $g->getSetting(
135✔
1124
                                            'moderated',
135✔
1125
                                            0
135✔
1126
                                        );
135✔
1127

1128
                                        $ps = ($u->isModOrOwner($group['groupid']) || $groupModerated) ? Group::POSTING_MODERATED : $ourPS;
135✔
1129
                                        $ps = $ps ? $ps : Group::POSTING_MODERATED;
135✔
1130
                                        if ($log)
135✔
1131
                                        {
1132
                                            error_log("Member of {$group['groupid']}, Our PS is $ps, group moderated? " . ($groupModerated ? 'yes': 'no'));
135✔
1133
                                        }
1134
                                    }
1135

1136
                                    if ($ps == Group::POSTING_MODERATED)
136✔
1137
                                    {
1138
                                        if ($log) { error_log("Mark as pending"); }
32✔
1139

1140
                                        if ($this->markPending($notspam))
32✔
1141
                                        {
1142
                                            $ret = MailRouter::PENDING;
32✔
1143
                                        }
1144
                                    } else
1145
                                    {
1146
                                        if ($log) { error_log("Mark as approved"); }
105✔
1147
                                        $ret = MailRouter::FAILURE;
105✔
1148

1149
                                        if ($this->markApproved())
105✔
1150
                                        {
1151
                                            $ret = MailRouter::APPROVED;
105✔
1152
                                        }
1153
                                    }
1154

1155
                                    # Record the posting of this message.
1156
                                    $sql = "INSERT INTO messages_postings (msgid, groupid, repost, autorepost) VALUES(?,?,?,?);";
136✔
1157
                                    $this->dbhm->preExec($sql, [
136✔
1158
                                        $this->msg->getId(),
136✔
1159
                                        $g->getId(),
136✔
1160
                                        0,
136✔
1161
                                        0
136✔
1162
                                    ]);
136✔
1163
                                } else {
1164
                                    # Not a member.  Reply to let them know.  This is particularly useful to
1165
                                    # Trash Nothing.
1166
                                    #
1167
                                    # This isn't a pretty mail, but it's not a very common case at all.
1168
                                    $this->mail(
4✔
1169
                                        $address,
4✔
1170
                                        $to,
4✔
1171
                                        "Message Rejected",
4✔
1172
                                        "You posted by email to $to, but you're not a member of that group.",
4✔
1173
                                        Mail::NOT_A_MEMBER,
4✔
1174
                                        $uid
4✔
1175
                                    );
4✔
1176
                                    $ret = MailRouter::DROPPED;
4✔
1177
                                }
1178
                            }
1179
                        }
1180

1181
                        if ($ret == MailRouter::DROPPED) {
143✔
1182
                            if ($log) { error_log("Not a member - drop it"); }
5✔
1183
                        }
1184
                    }
1185
                }
1186
            }
1187
        }
1188

1189
        return $ret;
143✔
1190
    }
1191

1192
    private function replyToSingleMessage($matches, bool $log, $ret, $spamfound)
1193
    {
1194
        if (!$this->msg->isBounce() && !$this->msg->isAutoreply())
4✔
1195
        {
1196
            $msgid = intval($matches[1]);
4✔
1197
            $fromid = intval($matches[2]);
4✔
1198

1199
            $m = new Message($this->dbhr, $this->dbhm, $msgid);
4✔
1200
            $groups = $m->getGroups(false, true);
4✔
1201
            $closed = false;
4✔
1202
            foreach ($groups as $gid) {
4✔
1203
                $g = Group::get($this->dbhr, $this->dbhm, $gid);
3✔
1204

1205
                if ($g->getSetting('closed', false))
3✔
1206
                {
1207
                    $closed = true;
1✔
1208
                }
1209
            }
1210

1211
            if ($closed)
4✔
1212
            {
1213
                if ($log)
1✔
1214
                {
1215
                    error_log("Reply to message on closed group");
1✔
1216
                }
1217
                $this->mail(
1✔
1218
                    $this->msg->getFromaddr(),
1✔
1219
                    NOREPLY_ADDR,
1✔
1220
                    "This community is currently closed",
1✔
1221
                    "This Freegle community is currently closed.\r\n\r\nThis is an automated message - please do not reply.",
1✔
1222
                    Mail::MODMAIL,
1✔
1223
                    $fromid
1✔
1224
                );
1✔
1225
                $ret = MailRouter::TO_SYSTEM;
1✔
1226
            } else {
1227
                # Find the latest entry in messages_history for this message.
1228
                $hist = $this->dbhr->preQuery("SELECT DATEDIFF(NOW(), arrival) AS daysago FROM messages_history WHERE msgid = ? ORDER BY id DESC LIMIT 1;", [
3✔
1229
                    $msgid
3✔
1230
                ]);
3✔
1231

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

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

1243
                    if ($m->getID() && $u->getId() && $m->getFromuser())
2✔
1244
                    {
1245
                        # The email address that we replied from might not currently be attached to the
1246
                        # other user, for example if someone has email forwarding set up.  So make sure we
1247
                        # have it.
1248
                        $u->addEmail($this->msg->getEnvelopefrom(), 0, false);
2✔
1249

1250
                        # The sender of this reply will always be on our platform, because otherwise we
1251
                        # wouldn't have generated a What's New mail to them.  So we want to set up a chat
1252
                        # between them and the sender of the message (who might or might not be on our
1253
                        # platform).
1254
                        if ($log)
2✔
1255
                        {
1256
                            error_log(
2✔
1257
                                "Create chat between " . $this->msg->getFromuser() . " (" . $this->msg->getFromaddr(
2✔
1258
                                ) . ") and $fromid for $msgid"
2✔
1259
                            );
2✔
1260
                        }
1261
                        $r = new ChatRoom($this->dbhr, $this->dbhm);
2✔
1262

1263
                        if ($fromid != $m->getFromuser()) {
2✔
1264
                            list ($chatid, $blocked) = $r->createConversation($fromid, $m->getFromuser());
2✔
1265

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

1269
                            if (strlen($textbody))
2✔
1270
                            {
1271
                                # Sometimes people will just email the photos, with no message.  We don't want to
1272
                                # create a blank chat message in that case, and such a message would get held
1273
                                # for review anyway.
1274
                                $cm = new ChatMessage($this->dbhr, $this->dbhm);
2✔
1275
                                list ($mid, $banned) = $cm->create(
2✔
1276
                                    $chatid,
2✔
1277
                                    $fromid,
2✔
1278
                                    $textbody,
2✔
1279
                                    ChatMessage::TYPE_INTERESTED,
2✔
1280
                                    $msgid,
2✔
1281
                                    false,
2✔
1282
                                    null,
2✔
1283
                                    null,
2✔
1284
                                    null,
2✔
1285
                                    null,
2✔
1286
                                    null,
2✔
1287
                                    $spamfound
2✔
1288
                                );
2✔
1289

1290
                                if ($mid)
2✔
1291
                                {
1292
                                    $cm->chatByEmail($mid, $this->msg->getID());
2✔
1293
                                }
1294
                            }
1295

1296
                            # Add any photos.
1297
                            $this->addPhotosToChat($chatid);
2✔
1298

1299
                            if ($m->hasOutcome())
2✔
1300
                            {
1301
                                # We don't want to email the recipient - no point pestering them with more
1302
                                # emails for items which are completed.  They can see them on the
1303
                                # site if they want.
1304
                                if ($log)
×
1305
                                {
1306
                                    error_log("Don't mail as promised to someone else $mid");
×
1307
                                }
1308
                                $r->mailedLastForUser($m->getFromuser());
×
1309
                            }
1310

1311
                            $ret = MailRouter::TO_USER;
2✔
1312
                        } else {
1313
                            if ($log) { error_log("Email reply to self"); }
×
1314
                            $ret = MailRouter::DROPPED;
×
1315
                        }
1316
                    }
1317
                }
1318
            }
1319
        }
1320
        return $ret;
4✔
1321
    }
1322

1323
    private function replyToChatNotification($matches, bool $log, $ret, $spamfound)
1324
    {
1325
        # It's a reply to an email notification.
1326
        $chatid = intval($matches[1]);
7✔
1327
        $userid = intval($matches[2]);
7✔
1328

1329
        $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid);
7✔
1330
        $u = User::get($this->dbhr, $this->dbhm, $userid);
7✔
1331

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

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

1364
                        foreach ($emails as $e) {
7✔
1365
                            if ($e['email'] == $this->msg->getEnvelopefrom()) {
7✔
1366
                                $found = TRUE;
3✔
1367
                                break;
3✔
1368
                            }
1369
                        }
1370

1371
                        if (!$found) {
7✔
1372
                            # The email address that we replied from might not currently be attached to the
1373
                            # other user, for example if someone has email forwarding set up.  In that case we'd want
1374
                            # to add it.
1375
                            #
1376
                            # But if mailboxes are hacked, then the notify- email might have been harvested and we
1377
                            # might get spammers mailing that address.  There's no really good way to spot this,
1378
                            # but if we get a mail from an unassociated email address to a chat which hasn't been
1379
                            # active for a while, then that's quite likely to be spam.
1380
                            $lastmessage = $r->getPrivate('latestmessage');
5✔
1381

1382
                            if ($lastmessage && (time() - strtotime($lastmessage) > User::OPEN_AGE * 24 * 60 * 60)) {
5✔
1383
                                # It's been a while since the last message, so this is probably spam.
1384
                                if ($log) { error_log("Spammy reply to chat $chatid from $userid"); }
1✔
1385
                                $ret = MailRouter::DROPPED;
1✔
1386
                            } else {
1387
                                # It's probably not spam, so add the email address.
1388
                                $u->addEmail($this->msg->getEnvelopefrom(), 0, false);
4✔
1389
                                $found = TRUE;
4✔
1390
                            }
1391
                        }
1392

1393
                        if ($found) {
7✔
1394
                            # Now add this into the conversation as a message.  This will notify them.
1395
                            $textbody = $this->msg->stripQuoted();
6✔
1396

1397
                            if (strlen($textbody))
6✔
1398
                            {
1399
                                # Sometimes people will just email the photos, with no message.  We don't want to
1400
                                # create a blank chat message in that case, and such a message would get held
1401
                                # for review anyway.
1402
                                $cm = new ChatMessage($this->dbhr, $this->dbhm);
6✔
1403
                                list ($mid, $banned) = $cm->create(
6✔
1404
                                    $chatid,
6✔
1405
                                    $userid,
6✔
1406
                                    $textbody,
6✔
1407
                                    ChatMessage::TYPE_DEFAULT,
6✔
1408
                                    null,
6✔
1409
                                    false,
6✔
1410
                                    null,
6✔
1411
                                    null,
6✔
1412
                                    null,
6✔
1413
                                    null,
6✔
1414
                                    null,
6✔
1415
                                    $spamfound
6✔
1416
                                );
6✔
1417

1418
                                if ($mid)
6✔
1419
                                {
1420
                                    $cm->chatByEmail($mid, $this->msg->getID());
6✔
1421
                                }
1422
                            }
1423

1424
                            # Add any photos.
1425
                            $this->addPhotosToChat($chatid);
6✔
1426

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

1431
                            $ret = MailRouter::TO_USER;
7✔
1432
                        }
1433
                    }
1434
                }
1435
            } else
1436
            {
1437
                if ($log)
×
1438
                {
1439
                    error_log("Bounce " . $this->msg->isBounce() . " auto " . $this->msg->isAutoreply());
×
1440
                }
1441
            }
1442
        }
1443
        return $ret;
7✔
1444
    }
1445

1446
    private function directMailToUser($u, $to, bool $log, $spamscore, $spamfound)
1447
    {
1448
        # See if it's a direct reply.  Auto-replies (that we can identify) we just drop.
1449
        $uid = $u->findByEmail($to);
21✔
1450
        if ($log)
21✔
1451
        {
1452
            error_log("Find direct reply from $to = user # $uid");
21✔
1453
        }
1454

1455
        if ($uid && $this->msg->getFromuser() && strtolower($to) != strtolower(MODERATOR_EMAIL))
21✔
1456
        {
1457
            # This is to one of our users.  We try to pair it as best we can with one of the posts.
1458
            #
1459
            # We don't want to process replies to ModTools user.  This can happen if MT is a member
1460
            # rather than a mod on a group.
1461
            $this->dbhm->background(
17✔
1462
                "UPDATE users SET lastaccess = NOW() WHERE id = " . $this->msg->getFromuser() . ";"
17✔
1463
            );
17✔
1464
            $original = $this->msg->findFromReply($uid);
17✔
1465
            if ($log)
17✔
1466
            {
1467
                error_log("Paired with $original");
17✔
1468
            }
1469

1470
            $ret = MailRouter::TO_USER;
17✔
1471

1472
            $textbody = $this->msg->stripQuoted();
17✔
1473

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

1478
            # Get/create the chat room between the two users.
1479
            if ($log)
17✔
1480
            {
1481
                error_log(
17✔
1482
                    "Create chat between " . $this->msg->getFromuser() . " (" . $this->msg->getFromaddr(
17✔
1483
                    ) . ") and $uid ($to)"
17✔
1484
                );
17✔
1485
            }
1486
            $r = new ChatRoom($this->dbhr, $this->dbhm);
17✔
1487
            list ($rid, $blocked) = $r->createConversation($this->msg->getFromuser(), $uid);
17✔
1488
            if ($log)
17✔
1489
            {
1490
                error_log("Got chat id $rid");
17✔
1491
            }
1492

1493
            if ($rid)
17✔
1494
            {
1495
                # Add in a spam score for the message.
1496
                if (!$spamscore)
17✔
1497
                {
1498
                    $this->spamc->command = 'CHECK';
16✔
1499
                    if ($this->spamc->filter($this->msg->getMessage()))
16✔
1500
                    {
1501
                        $spamscore = $this->spamc->result['SCORE'];
16✔
1502
                        if ($log)
16✔
1503
                        {
1504
                            error_log("Spam score $spamscore");
16✔
1505
                        }
1506
                    }
1507
                }
1508

1509
                # And now add our text into the chat room as a message.  This will notify them.
1510
                $m = new ChatMessage($this->dbhr, $this->dbhm);
17✔
1511
                list ($mid, $banned) = $m->create(
17✔
1512
                    $rid,
17✔
1513
                    $this->msg->getFromuser(),
17✔
1514
                    $textbody,
17✔
1515
                    $this->msg->getModmail() ? ChatMessage::TYPE_MODMAIL : ChatMessage::TYPE_INTERESTED,
17✔
1516
                    $original,
17✔
1517
                    false,
17✔
1518
                    $spamscore,
17✔
1519
                    null,
17✔
1520
                    null,
17✔
1521
                    null,
17✔
1522
                    null,
17✔
1523
                    $spamfound
17✔
1524
                );
17✔
1525
                if ($log)
17✔
1526
                {
1527
                    error_log("Created chat message $mid");
17✔
1528
                }
1529

1530
                $m->chatByEmail($mid, $this->msg->getID());
17✔
1531

1532
                # Add any photos.
1533
                $this->addPhotosToChat($rid);
17✔
1534

1535
                if ($original)
17✔
1536
                {
1537
                    $m = new Message($this->dbhr, $this->dbhm, $original);
15✔
1538

1539
                    if ($m->hasOutcome())
15✔
1540
                    {
1541
                        # We don't want to email the recipient - no point pestering them with more
1542
                        # emails for items which are completed.  They can see them on the
1543
                        # site if they want.
1544
                        if ($log)
×
1545
                        {
1546
                            error_log("Don't mail as promised to someone else $mid");
×
1547
                        }
1548
                        $r->mailedLastForUser($m->getFromuser());
17✔
1549
                    }
1550
                }
1551
            }
1552
        } else {
1553
            if ($log) { error_log("Not to group and not reply - drop"); }
4✔
1554
            $ret = MailRouter::DROPPED;
4✔
1555
        }
1556

1557
        return $ret;
21✔
1558
    }
1559

1560
    public function setLatLng($lat, $lng) {
1561
        $this->msg->setPrivate('lat', $lat);
16✔
1562
        $this->msg->setPrivate('lng', $lng);
16✔
1563
    }
1564
}
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