Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/_email_send.pl
Diff

/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/_email_send.pl

added on local at 2026-07-01 12:35:20

Added
+195
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to c65a8ea50069
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# TaskForge -- Email outbound worker
4#
5# Drains email_outbound_queue every minute. For each queued row:
6# 1. Decrypt the inbox's SMTP password
7# 2. Send via Net::SMTP (STARTTLS or implicit SSL based on port + flag)
8# 3. On success: mark sent_at + status=sent, record Message-ID for
9# future threading
10# 4. On failure: bump attempts, store last_error. Give up after 5 tries.
11#
12# Crontab (per minute):
13# * * * * * /usr/bin/perl /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/_email_send.pl >> /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/_email_send.log 2>&1
14#======================================================================
15use strict;
16use warnings;
17use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com';
18use MODS::DBConnect;
19use MODS::PTMatrix::Config;
20use MODS::PTMatrix::EmailInbox;
21
22$| = 1;
23$ENV{SCRIPT_NAME} ||= '_email_send.pl';
24
25my $db = MODS::DBConnect->new;
26my $cfg = MODS::PTMatrix::Config->new;
27my $ei = MODS::PTMatrix::EmailInbox->new;
28my $DB = $cfg->settings('database_name');
29
30my $dbh = $db->db_connect();
31exit 1 unless $dbh;
32unless ($ei->schema_ready($db, $dbh, $DB)) { $db->db_disconnect($dbh); exit 0; }
33
34# Detect the optional platform_relay_enabled column so we can pull it
35# when present (older installs without the migration get an implicit 0).
36my $has_relay_col = $db->db_readwrite($dbh, qq~
37 SELECT COUNT(*) AS n FROM information_schema.COLUMNS
38 WHERE TABLE_SCHEMA='$DB' AND TABLE_NAME='email_inboxes'
39 AND COLUMN_NAME='platform_relay_enabled'
40~, $ENV{SCRIPT_NAME}, __LINE__);
41my $relay_col_sql = ($has_relay_col && $has_relay_col->{n}) ? ', i.platform_relay_enabled' : '';
42
43# Forwarding-kind inboxes have no own SMTP -- they always use the platform
44# relay. Pull inbox_kind when the column exists.
45my $has_kind_col = $db->db_readwrite($dbh, qq~
46 SELECT COUNT(*) AS n FROM information_schema.COLUMNS
47 WHERE TABLE_SCHEMA='$DB' AND TABLE_NAME='email_inboxes'
48 AND COLUMN_NAME='inbox_kind'
49~, $ENV{SCRIPT_NAME}, __LINE__);
50my $kind_col_sql = ($has_kind_col && $has_kind_col->{n}) ? ', i.inbox_kind, i.forward_address' : '';
51
52# Load the platform relay config once (cheap, cached for the cycle).
53my %relay;
54foreach my $k (qw(host port tls user pw_enc from from_name)) {
55 my $r = $db->db_readwrite($dbh, qq~
56 SELECT setting_value FROM `${DB}`.platform_settings
57 WHERE setting_key='email.platform_relay_$k' LIMIT 1
58 ~, $ENV{SCRIPT_NAME}, __LINE__);
59 $relay{$k} = ($r && defined $r->{setting_value}) ? $r->{setting_value} : '';
60}
61
62# Pull up to 20 queued items per tick. attempts < 5 stops infinite loops.
63my @queue = $db->db_readwrite_multiple($dbh, qq~
64 SELECT o.*, i.smtp_host, i.smtp_port, i.smtp_use_tls,
65 i.smtp_user, i.smtp_password_enc, i.smtp_from_email, i.smtp_from_name,
66 i.imap_user, i.company_id$relay_col_sql$kind_col_sql
67 FROM `${DB}`.email_outbound_queue o
68 JOIN `${DB}`.email_inboxes i ON i.id = o.inbox_id
69 WHERE o.status='queued' AND o.attempts < 5
70 ORDER BY o.queued_at ASC
71 LIMIT 20
72~, $ENV{SCRIPT_NAME}, __LINE__);
73
74print _log(sprintf("sending %d outbound\n", scalar(@queue)));
75
76foreach my $row (@queue) {
77 eval { send_one($row); };
78 if ($@) {
79 my $err = $@; $err =~ s/'/''/g; $err = substr($err, 0, 500);
80 $db->db_readwrite($dbh, qq~
81 UPDATE `${DB}`.email_outbound_queue
82 SET attempts=attempts+1,
83 last_error='$err',
84 status=IF(attempts>=4, 'failed', 'queued')
85 WHERE id='$row->{id}'
86 ~, $ENV{SCRIPT_NAME}, __LINE__);
87 print _log("queue $row->{id}: $err\n");
88 }
89}
90
91$db->db_disconnect($dbh);
92exit 0;
93
94sub send_one {
95 my ($row) = @_;
96
97 # Three reasons to use the platform relay:
98 # 1. Super-admin flipped the platform_relay_enabled flag.
99 # 2. Inbox kind is 'forwarding' -- they have NO own SMTP, the
100 # platform relay is their outbound path by definition.
101 # 3. Same for OAuth/native-API kinds in the per-kind workers, but
102 # those don't reach this branch yet.
103 # In all cases, "From" stays the inbox's from_email so replies route
104 # back to the customer's address.
105 my $is_forwarding = ($row->{inbox_kind} && $row->{inbox_kind} eq 'forwarding') ? 1 : 0;
106 my $use_relay = (
107 ($row->{platform_relay_enabled} && $relay{host}) ||
108 ($is_forwarding && $relay{host})
109 ) ? 1 : 0;
110
111 my ($inbox, $pw);
112 if ($use_relay) {
113 $pw = length $relay{pw_enc}
114 ? $ei->decrypt_password($db, $dbh, $DB, $relay{pw_enc})
115 : '';
116 # Anonymous relay (no auth) is allowed when relay_user is blank,
117 # so don't die on missing password unless user is set.
118 if (length $relay{user} && !length $pw) {
119 die "platform relay configured with user but no password\n";
120 }
121 $inbox = {
122 smtp_host => $relay{host},
123 smtp_port => ($relay{port} || 587),
124 smtp_use_tls => ($relay{tls} && $relay{tls} ne '0') ? 1 : 0,
125 smtp_user => $relay{user} || '',
126 smtp_from_email => $row->{smtp_from_email} || $relay{from} || $row->{imap_user},
127 smtp_from_name => $row->{smtp_from_name} || $relay{from_name} || 'PTMatrix',
128 imap_user => $row->{imap_user},
129 };
130 print _log("queue $row->{id}: routing via PLATFORM RELAY ($relay{host})\n");
131 } else {
132 die "no smtp_host configured for inbox $row->{inbox_id}\n" unless $row->{smtp_host};
133 $pw = $ei->decrypt_password($db, $dbh, $DB, $row->{smtp_password_enc});
134 die "could not decrypt smtp password\n" unless length $pw;
135 $inbox = {
136 smtp_host => $row->{smtp_host},
137 smtp_port => $row->{smtp_port},
138 smtp_use_tls => $row->{smtp_use_tls},
139 smtp_user => $row->{smtp_user},
140 smtp_from_email => $row->{smtp_from_email} || $row->{imap_user},
141 smtp_from_name => $row->{smtp_from_name} || '',
142 imap_user => $row->{imap_user},
143 };
144 }
145
146 my $msgid_pre = $row->{message_id} || '';
147 my ($ok, $msg_or_err) = $ei->send_smtp($inbox, $pw,
148 to => $row->{to_addr},
149 subject => $row->{subject},
150 body => $row->{body_text},
151 in_reply_to => $row->{in_reply_to},
152 message_id => $msgid_pre,
153 ticket_id => $row->{task_id},
154 );
155 if ($ok) {
156 my $mid_q = $msg_or_err; $mid_q =~ s/'/''/g;
157 $db->db_readwrite($dbh, qq~
158 UPDATE `${DB}`.email_outbound_queue
159 SET status='sent', sent_at=NOW(), attempts=attempts+1,
160 last_error=NULL, message_id='$mid_q'
161 WHERE id='$row->{id}'
162 ~, $ENV{SCRIPT_NAME}, __LINE__);
163
164 # Also record the outbound in email_messages so future inbound
165 # replies threading on this Message-ID find the right task.
166 my $task_sql = $row->{task_id} ? "'$row->{task_id}'" : 'NULL';
167 my $subj_q = $row->{subject} || ''; $subj_q =~ s/'/''/g; $subj_q = substr($subj_q, 0, 500);
168 my $body_q = $row->{body_text} || ''; $body_q =~ s/'/''/g; $body_q = substr($body_q, 0, 64000);
169 my $to_q = $row->{to_addr} || ''; $to_q =~ s/'/''/g;
170 $db->db_readwrite($dbh, qq~
171 INSERT INTO `${DB}`.email_messages
172 SET inbox_id='$row->{inbox_id}',
173 task_id=$task_sql,
174 direction='outbound',
175 message_id='$mid_q',
176 to_addr='$to_q',
177 from_addr='$inbox->{smtp_from_email}',
178 from_name='$inbox->{smtp_from_name}',
179 subject='$subj_q',
180 body_text='$body_q',
181 processed_at=NOW()
182 ~, $ENV{SCRIPT_NAME}, __LINE__);
183
184 print _log("queue $row->{id} sent to $row->{to_addr}\n");
185 } else {
186 die $msg_or_err . "\n";
187 }
188}
189
190sub _log {
191 my ($msg) = @_;
192 my @t = localtime;
193 return sprintf("[%04d-%02d-%02d %02d:%02d:%02d] %s",
194 $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0], $msg);
195}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help