Live, while it is happening

The engine room

Every message below is a real send, the moment it happens. The code is the real sender, and the highlight follows the checks each message passes before it is allowed to leave.

—
sent
—
still to go
—
per hour
—
addressed by name
—
refused before sending
—
finishes about
scripts/outreach/send-gmail.mjswaiting for the next send…
453 order by d.decided_at asc
454 limit $1`, [room, CAMPAIGN_FILTER, SENDER]);
455console.log(`approved and unsent for ${SENDER}: ${approved.length}`);
456if (!approved.length) { await sql.end(); process.exit(0); }
457
458const { rows: sendable } = await sql.query('select lower(email) email from v_sendable');
459const okAddr = new Set(sendable.map((s) => s.email));
460
461let sent = 0, skipped = 0;
462/* A database blip must cost one message, not the whole night.
463 *
464 * thunga@ died at 06:07 on EADDRNOTAVAIL — a transient failure opening a
465 * connection to Supabase — with 100 messages still to send. Every Gmail call in
466 * this file already retries, because the network between here and Google was
467 * expected to wobble. The network between here and the DATABASE was not, so a
468 * single failed query threw out of the loop and took the process with it.
469 *
470 * The pilot this was written for made seven queries over 22 minutes. This run
471 * makes four per message for eight hours. At that length "it usually works" is
472 * not a property you can rely on, and the supervisor restarting the process is
473 * a coarse recovery: it loses whatever was in flight and waits up to fifteen
474 * minutes to notice. Catching here is the cheap, precise version. */
475const { existsSync } = await import('node:fs');
476const stopped = () => STOP_FILE && existsSync(STOP_FILE);
477/* Sleep in 10-second steps so a stop file ends the wait, not just the next send. */
478const humanWait = async (ms) => { const end = Date.now() + ms; while (Date.now() < end) { if (stopped()) return; await new Promise((r) => setTimeout(r, Math.min(10000, end - Date.now()))); } };
479let nextBreakAt = 12 + Math.floor(Math.random() * 7);
480for (const d of approved) {
481 try {
482 if (stopped()) { console.log(`STOP FILE ${STOP_FILE} present - ending the run before ${d.to_email}`); break; }
483 if (HUMAN) {
484 /* Re-prove THIS address now, not at start: the loaded set is minutes or hours old. */
485 const { rows: [live] } = await sql.query(
486 `select exists (select 1 from v_sendable where lower(email) = lower($1)) ok,
487 exists (select 1 from agent_state where agent_id = $2 and status::text = 'do_not_contact') dnc`, [d.to_email, d.agent_id]);
488 if (!live.ok || live.dnc) okAddr.delete(d.to_email.toLowerCase());
489 }
490 if (!okAddr.has(d.to_email.toLowerCase())) {
491 await sql.query(
492 `update outreach_drafts set status = 'rejected',
493 why_this_company = why_this_company || ' | SKIPPED AT SEND: address no longer in v_sendable'
494 where id = $1`, [d.id]);
495 console.log(` SKIP ${d.company} — ${d.to_email} left v_sendable since approval`);
496 skipped++;
497 continue;
498 }
499
500 if (!REALLY) {
501 console.log(` would send ${d.company.padEnd(40)} -> ${d.to_email} "${d.subject}"`);
502 continue;
503 }
504
505 /* CLAIM IT. Three senders run at once and a keeper restarts any that dies;
506 pgrep is a race, so a twin can exist for an instant. "select approved, then
507 send" is read-then-write with a gap, and in that gap two processes send the
508 same mail to the same partner. Moving approved -> sending in ONE statement
509 closes it: whoever gets the row owns it, the other gets zero rows and walks
510 on. This is the never-mail-twice rule made structural. */
511 const claim = await sql.query(
512 `update outreach_drafts set status = 'sending'
513 where id = $1 and status = 'approved' returning id`, [d.id]);
514 if (!claim.rowCount) {
515 console.log(` claimed by another sender, skipping ${d.company}`);
516 continue;
517 }
518
519 /* Belt and braces: has this exact address already had this touch, under any
520 draft row? The unique index would refuse it at the end anyway, but finding
521 out BEFORE transmitting is the difference between a blocked write and a
522 delivered duplicate. */
523 const dup = await sql.query(
524 `select 1 from outreach_drafts
525 where lower(to_email) = lower($1) and campaign = $2 and touch = $3
526 and status = 'sent' limit 1`, [d.to_email, d.campaign, d.touch]);
527 if (dup.rowCount) {
528 await sql.query(
529 `update outreach_drafts set status = 'rejected',
530 why_this_company = why_this_company || ' | ALREADY SENT to this address for this touch'
531 where id = $1`, [d.id]);
532 console.log(` ALREADY SENT to ${d.to_email} — refusing to send twice`);
533 skipped++;
534 continue;
535 }
536
537 const cc = CC_LIST;
538 // The cross-system double-send guard. The ALREADY SENT check above asks OUR
539 // database; this asks the shared ledger, so a mail a Claude session sent to
540 // this same person an hour ago — which the database knows nothing about —
541 // still stops the campaign writing to them again. Releases the claim first
542 // so the row is not parked in 'sending'. See scripts/mail/mail-guard.mjs.
543 try { await guard({ to: d.to_email, cc, subject: d.subject }); }
544 catch (e) {
545 if (!(e instanceof GuardBlocked)) throw e;
546 await sql.query(`update outreach_drafts set status = 'approved' where id = $1 and status = 'sending'`, [d.id]);
547 console.error(`\n ${d.company}: ${e.message}`);
548 skipped++;
549 continue;
550 }
551 const raw = HUMAN && !CARD
552 ? mime(d.to_email, d.subject, d.body, null, d.campaign, d.touch, cc, null, null)
553 : mime(d.to_email, d.subject, d.body, sigHtml ? htmlBody(d.body) : null,
554 d.campaign, d.touch, cc, ATTACH, INLINE);
555 // A transient ETIMEDOUT on one call must not kill the run (it did, 10 Aug —
556 // 2 of 49 sent, process dead). Retry the network; skip the draft on
557 // persistent failure — it stays approved and the next run picks it up.
558 let res = null;
559 for (let attempt = 1; attempt <= 3; attempt++) {
560 try {
561 // 60s hard timeout: a dead-air connection hung the 10 Aug night run for
562 // 8 hours. A send that times out is treated as NOT sent; reconcile
563 // against the mailbox's real Sent folder before ever assuming otherwise.
564 res = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', {
565 method: 'POST',
566 headers: { authorization: `Bearer ${await token()}`, 'content-type': 'application/json' },
567 body: JSON.stringify({ raw: b64url(raw) }),
568 signal: AbortSignal.timeout(60000),
569 });
570 break;
571 } catch (e) {
572 console.error(` network error for ${d.company} (attempt ${attempt}/3): ${e.cause?.code || e.message}`);
573 // THE CONVEYOR LESSON (10 Aug): a send whose response never arrives may
574 // still have TRANSMITTED. Before any retry, ask the mailbox itself.
575 try {
576 const chk = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages?' +
577 new URLSearchParams({ q: `in:sent to:${d.to_email} newer_than:1d`, maxResults: '1' }),
578 { headers: { authorization: `Bearer ${await token()}` }, signal: AbortSignal.timeout(30000) });
579 if (chk.ok && ((await chk.json()).resultSizeEstimate || 0) > 0) {
580 console.error(` ${d.company}: found in Sent despite the error — marking sent, NOT retrying`);
581 res = { ok: true, alreadyInSent: true };
582 break;
The six checks below run for every single message. Any one of them can stop it.
Sending nowconnecting…
  1. waiting…
Re-prove the address
The never-mail-twice list is checked again at the moment of sending, not at approval. An address that left it since is skipped and flagged.
Claim the draft
approved → sending in one statement. Two senders run at once tonight; whoever gets the row owns it, the other walks on. This is what makes a double send impossible.
Refuse a second send
Has this exact address already had this touch? The database also enforces it with a unique index, but finding out BEFORE transmitting is the difference between a blocked write and a delivered duplicate.
Build the message
Plain text and HTML, the signature carried inside the message as a baked card — pixels, because they are the one thing every mail client renders alike.
Hand it to Gmail
A 60-second deadline, three retries — and before any retry it asks the mailbox’s own Sent folder whether the message already went, because a response that never arrives is not the same as a message that never sent.
Record it, or none of it
sent_at, the company timeline entry and the relationship state move together in one transaction, or nothing moves. The record can never claim a mail that did not go.
This run’s supervisor — the code that guarantees the run startsscripts/outreach/run-egypt.sh, the real file
1#!/bin/bash
2# The EGYPT introduction send — supervisor for an unattended morning run (cloned from run-usa.sh, 6 Sep 2026).
3#
4#   ./scripts/outreach/run-egypt.sh --rehearse   # ONE live message per mailbox, now
5#   ./scripts/outreach/run-egypt.sh              # wait for the scheduled hour, then send
6#   ./scripts/outreach/run-egypt.sh --now        # skip the wait, send immediately
7#   touch /tmp/eg-stop                             # ask it to stop cleanly
8#   tail -f /tmp/eg-send.log                       # watch it
9#
10# WHY THIS FILE EXISTS
11#
12# Two things kill a long overnight job on this laptop, and neither is the code:
13#
14#   1. THE MAC GOING TO SLEEP. Plain caffeinate does NOT survive a closed lid. This
15#      project has lost an 8-hour run and a 4.5-hour run to exactly that, and the
16#      damning part is that a sleeping Mac looks identical to a healthy one in the
17#      morning. So this refuses to start unless the machine is on mains power, warns
18#      loudly about the lid, and writes a heartbeat every 60 seconds — a stalled run
19#      and a working run are only distinguishable if something is ticking.
20#
21#   2. THE PROCESS DYING. A hotspot drop killed both senders twice on the India night.
22#      Nothing was lost, because an unsent draft stays 'approved' and an in-flight
23#      claim is released — but nobody restarted them for fifteen minutes. This loop
24#      restarts a dead sender in seconds.
25#
26# It cannot decide to send anything. send-gmail.mjs reads only 'approved' drafts, and
27# approving is a separate, deliberate act.
28
29set -u
30cd "$(dirname "$0")/../.." || exit 1
31
32# THE WHOLE RUN HOLDS A SLEEP ASSERTION, from the first second. The battery-wait loop
33# originally ran bare, and a Mac on battery idle-sleeps within minutes — it would then
34# sleep through the plug-in, the hour, and the whole night, while the log's last line
35# says "waiting for the charger" and looks perfectly healthy. caffeinate -ims: -i stops
36# idle sleep (honoured on battery too), -m disk, -s system sleep on AC. The display may
37# sleep; that is fine and spares the battery. A closed lid still sleeps the machine —
38# that is the one thing no userspace process can hold off.
39if [ -z "${WZ_CAFFEINATED:-}" ]; then
40  export WZ_CAFFEINATED=1
41  exec caffeinate -ims "$0" "$@"
42fi
43
44CAMPAIGN='eg-intro-2026-09'
45LOG=/tmp/eg-send.log
46BEAT=/tmp/eg-heartbeat
47STOP=/tmp/eg-stop
48GAP=45
49CAP=800
50
51# 10:00 Dubai Monday 7 September 2026 = 06:00 UTC = 09:00 Cairo (EEST, UTC+3).
52# The send opens exactly as the Egyptian Monday begins; Egypt works Sunday to Thursday.
53START_EPOCH=1788760800
54
55REHEARSE=0; NOW=0
56for a in "$@"; do
57  case "$a" in
58    --rehearse) REHEARSE=1 ;;
59    --now)      NOW=1 ;;
60    *) echo "unknown flag: $a"; exit 1 ;;
61  esac
62done
63
64say() { echo "$(date '+%F %T %Z')  $*" | tee -a "$LOG"; }
65
66# ---- guards that fail LOUDLY, before anything is scheduled -------------------
67
68# Are there actually approved drafts to send? A supervisor that cheerfully runs all
69# night over an empty queue is the same lie as a green check that never reached the app.
70REMAINING=$(node scripts/outreach/eg-status.mjs --remaining 2>/dev/null | tail -1)
71case "$REMAINING" in
72  ''|*[!0-9]*) say "REFUSING TO START: could not read the approved count from the database"; exit 1 ;;
73esac
74if [ "$REMAINING" -eq 0 ] && [ "$REHEARSE" -eq 0 ]; then
75  say "REFUSING TO START: 0 approved and unsent drafts for $CAMPAIGN."
76  say "                   Stage and approve first — this script does not approve anything."
77  exit 1
78fi
79# Do NOT claim power/lid state here — those checks now run below, after the rehearsal
80# branch. Saying "on AC power · lid open" before testing either was a log line asserting
81# something it had not verified.
82say "$REMAINING approved and unsent for $CAMPAIGN"
83
84# ---- rehearsal: prove the whole chain with ONE real message per mailbox ------
85if [ "$REHEARSE" -eq 1 ]; then
86  say "REHEARSAL — one real message per mailbox to thunga@worldzone.ae, nothing marked sent"
87  for pair in "globalsales:thunga@worldzone.ae" "me:globalsales@worldzone.ae"; do
88    acct="${pair%%:*}"; cc="${pair##*:}"
89    say "  rehearsing $acct (cc $cc)"
90    caffeinate -dimsu node scripts/outreach/send-gmail.mjs --send \
91      --account "$acct" --campaign "$CAMPAIGN" \
92      --cc "$cc" --test thunga@worldzone.ae 2>&1 | tee -a "$LOG"
93  done
94  say "REHEARSAL DONE — open both messages on phone AND desktop and read them"
95  exit 0
96fi
97
98# ---- the unattended-run guards -----------------------------------------------
99# These protect a FIVE-HOUR run on a laptop. They are deliberately checked here, after
100# the rehearsal branch has already exited: a rehearsal is two messages and a few
101# seconds, and refusing to let him test because the charger is out of reach is a guard
102# working against the person it protects.
103# On mains power? -s (system sleep) is only honoured on AC, so a battery run sleeps.
104#
105# WAIT rather than refuse. This is started in the evening and fires at 02:00, so the
106# charger may simply not be plugged in yet — refusing outright means somebody has to
107# remember to come back and start it again, which is exactly the step that gets
108# forgotten at bedtime. So it polls, and only gives up if the start time actually
109# arrives with the machine still on battery.
110if ! pmset -g ps | grep -q 'AC Power'; then
111  say "on BATTERY — waiting for the charger (will start itself once plugged in)"
112  while ! pmset -g ps | grep -q 'AC Power'; do
113    if [ -f "$STOP" ]; then say "stop file while waiting for power — exiting"; exit 0; fi
114    if [ "$(date +%s)" -ge "$START_EPOCH" ]; then
115      say "REFUSING TO START: start time reached and this Mac is still on battery."
116      say "                   caffeinate -s is ignored on battery and it would sleep mid-run."
117      exit 1
118    fi
119    date '+%F %T' > "$BEAT"
120    sleep 30
121  done
122  say "charger detected — continuing"
123fi
124
125# Lid closed? Clamshell mode with no external display means sleep, whatever caffeinate says.
126# Same treatment for the lid: wait for it to be opened rather than abandon the night.
127CLAM=$(ioreg -r -k AppleClamshellState 2>/dev/null | grep -m1 AppleClamshellState | grep -o 'Yes\|No')
128if [ "$CLAM" = "Yes" ]; then
129  say "lid is CLOSED — waiting for it to be opened"
130  while [ "$(ioreg -r -k AppleClamshellState 2>/dev/null | grep -m1 AppleClamshellState | grep -o 'Yes\|No')" = "Yes" ]; do
131    if [ -f "$STOP" ]; then say "stop file while waiting for the lid — exiting"; exit 0; fi
132    if [ "$(date +%s)" -ge "$START_EPOCH" ]; then
133      say "REFUSING TO START: start time reached with the lid CLOSED. This has cost this"
134      say "                   project two overnight runs already."
135      exit 1
136    fi
137    date '+%F %T' > "$BEAT"
138    sleep 30
139  done
140  say "lid open — continuing"
141fi
142
143
144# ---- wait for the hour ------------------------------------------------------
145rm -f "$STOP"
146if [ "$NOW" -eq 0 ]; then
147  NOW_EPOCH=$(date +%s)
148  WAIT=$((START_EPOCH - NOW_EPOCH))
149  if [ "$WAIT" -gt 0 ]; then
150    say "waiting ${WAIT}s until $(date -r $START_EPOCH '+%F %T %Z') (10:00 Dubai Mon 7 Sep = 09:00 Cairo)"
151    # Sleep in one-minute steps under caffeinate, beating the whole time, so the wait
152    # itself cannot be where the Mac quietly dozes off.
153    caffeinate -dimsu bash -c "
154      while [ \$(date +%s) -lt $START_EPOCH ]; do
155        [ -f '$STOP' ] && exit 3
156        date '+%F %T' > '$BEAT'
157        sleep 60
158      done" || { say "stopped during the wait"; exit 0; }
159  else
160    say "start time already passed by $((-WAIT))s — starting now"
161  fi
162fi
163
164# ---- send -------------------------------------------------------------------
165say "=== starting both mailboxes · gap ${GAP}s · cap ${CAP}/mailbox/day ==="
166
167send_loop() {
168  local acct="$1" cc="$2" box="$3"
169  while true; do
170    [ -f "$STOP" ] && { say "[$box] stop file — exiting"; return 0; }
171    local left
172    left=$(node scripts/outreach/eg-status.mjs --remaining 2>/dev/null | tail -1)
173    case "$left" in ''|*[!0-9]*) left=-1 ;; esac
174    if [ "$left" -eq 0 ]; then say "[$box] nothing left approved — done"; return 0; fi
175
176    say "[$box] starting sender ($left left across both boxes)"
177    caffeinate -dimsu node scripts/outreach/send-gmail.mjs --send \
178      --account "$acct" --campaign "$CAMPAIGN" \
179      --cc "$cc" --cap "$CAP" --gap "$GAP" >> "$LOG" 2>&1
180    local rc=$?
181    say "[$box] sender exited rc=$rc"
182    # A clean exit with nothing left means finished. Anything else: pause, go again.
183    sleep 20
184  done
185}
186
187# Heartbeat, independent of the senders. If this file stops advancing the run is
188# stalled or the Mac is asleep, whatever the log's last line says.
189( while [ ! -f "$STOP" ]; do date '+%F %T' > "$BEAT"; sleep 60; done ) &
190BEAT_PID=$!
191
192send_loop globalsales thunga@worldzone.ae      "globalsales@" &
193P1=$!
194send_loop me          globalsales@worldzone.ae "thunga@" &
195P2=$!
196
197wait $P1 $P2
198kill $BEAT_PID 2>/dev/null
199
200say "=== both senders finished ==="
201node scripts/outreach/eg-status.mjs 2>&1 | tee -a "$LOG"
202say "Now prove the Mac stayed awake:  pmset -g log | grep -iE 'Entering Sleep|Wake from'"
203
Every guard here is a previous failure made structural: it holds a sleep assertion from its first second (a bare wait loop once meant the Mac could doze on battery), it WAITS for the charger and the lid instead of refusing, it writes a heartbeat every 60s so a stalled run cannot impersonate a healthy one, and it restarts a sender that dies within seconds — a dropped connection killed both senders twice on an earlier run and nobody noticed for fifteen minutes. It also refuses to start at all when nothing has been approved, because a supervisor that runs cheerfully over an empty queue is the same lie as a green check that never touched the data. What no software can do: open a closed lid.

How a campaign goes out safely

Sending a thousand emails is easy. The work is in what the system refuses to do — and every refusal below is enforced in code, not in someone remembering. These are the real numbers from this campaign.

764
Egyptian freight companies on file
Harvested from the EIFFA and FIATA registers, WCA, PPL and the company websites themselves.
→
645
have an address we can prove
119 have none. We do not guess one — a blank beats a wrong address, always.
→
630
also publish a phone
The phone is never dialled here. It is a quality signal: a company maintaining both is a company whose details are current.
→
100
held back before sending
35 wrong or dead recipients pulled by the pre-send audit — a maritime magazine, a letter that would have been delivered to Italy, eleven domains sold to investors — plus 59 firms our desks are already in conversation with and six Egyptian arms of global forwarders.
→
—
actually sent
— of them opened with a real person's name rather than "Dear Sir/Madam".
01

Nobody can be written to twice

Three programs send at once, and a supervisor restarts any that dies. That creates a window where two copies of the same sender could exist for an instant and both pick up the same company. Rather than trust that the window is small, the rule is enforced twice: the database physically refuses a second send, and each message is claimedin a single statement before it is built.

create unique index outreach_one_send_per_address
  on outreach_drafts (lower(to_email), campaign, touch)
  where status = 'sent';

In plain terms: once a message to an address has been recorded as sent, the database will reject any attempt to record a second one. Not warn — reject. We proved it by trying: it refused.

update outreach_drafts set status = 'sending'
 where id = $1 and status = 'approved'
 returning id;

And this is the claim. Whichever program updates the row first owns that message; the other gets nothing back and moves on. There is no moment where both think it is theirs.

02

Every name has to be earned

A greeting is the first thing anyone reads, so a wrong name is worse than no name. Names come from three sources, in order of how well they are proved: a contact already on file, the company's WCA profile, or the address itself — pankaj.gupta@ is Pankaj, and nobody typed that.

The third source is where a careless version does damage. The first run of it produced "Dear Privacy,", "Dear Hello," and "Dear Cochin,". So a bare word now has to be corroborated against the 1,986 real first names already in the contact record before it counts as a person:

if (parts.length === 1 && !FIRST_NAMES.has(first)) return null;

It also caught the opposite mistake: 42 contacts are stored as "Mr. Sunil Kapoor", which would have opened 42 emails with "Dear Mr.,". Stripping the title properly gained 83 usable names.470 messages carry a name; the rest honestly say Sir/Madam.

03

The pace is arithmetic, not a guess

Google allows 2,000 recipients per account per day. Every message here carries two colleagues on copy, so one message is three recipients — which is why the limit is counted in recipients and not in emails.

Split across three mailboxes at one message a minute each, the run is roughly 500 messages per account and finishes in about eight hours, using around 1,500 of each account's 2,000 recipients. The interval is randomised by ±12.5% so it reads as a person working, not a machine firing on a metronome:

await new Promise((r) => setTimeout(
  r, GAP * 1000 * (0.875 + Math.random() * 0.25)));
04

It stops itself when it should

A supervisor checks every fifteen minutes and can halt everything. It distinguishes two failures that look identical in a summary and are not remotely the same thing.

A dead address costs one wasted message and tells Google nothing about us — every scraped list has them. Aspam block is Google saying it does not trust the sender, and that damages the domain the company runs its shipments on. Holding both to one number meant an ordinary stale list could halt a healthy campaign — which is exactly what happened at 00:26, and why the thresholds are now separate:

if [ "$spct" -ge 2 ];  then  # spam blocks — stop, this is reputation
if [ "$hpct" -ge 15 ]; then  # dead addresses — stop, the list is too stale

It fired once, at 00:26, on a bounce rate of 8%. Reading the failures rather than counting them changed the answer: every one was a dead address and not a single message had been blocked as spam. The campaign was in no trouble; the threshold was wrong. That is the version running now.

05

A send that is not recorded did not happen

The moment a message leaves, three things have to be written: the message is marked sent, a dated line is added to that company's timeline, and the relationship state moves to "emailed". They move together in one transaction, or none of them move.

This is why the counter on this page can be trusted. It is not a tally the sender keeps in memory — it is the same rows that were written inside the transmission itself. The record cannot claim a message that did not go, and cannot forget one that did.

06

The failure nobody thinks about

A Google access token lives one hour. This sender was originally written for a 30-message pilot that finished in 22 minutes, so it fetched one token at startup and never thought about it again.

At one message a minute for eight hours, that token dies at message 31 — at half past midnight, with nobody awake, and every send after it failing silently until morning. It now renews itself at the 45-minute mark, before the hour is up:

if (!tokenValue || Date.now() - tokenMintedAt > 45 * 60 * 1000) {
  tokenValue = await gmailToken();   // and it re-proves the mailbox
}

And it re-proves, every time, that the token opens the mailbox it claims to. A sender that quietly writes from the wrong account is a worse outcome than one that stops.