Как да спреш изходящата поща на Mac (Postfix)
Всеки път, когато отварях терминала, ме посрещаше съобщението "You have mail". Оказа се, че пощенската кутия е почти 100 MB върнати имейли.
Причината: тестовете на един plugin вървяха директно на Mac-а, с PHP от Homebrew, а не в Docker. Контейнерите ми имат фалшив sendmail, който записва всяко писмо във файл. Самият Mac - не. Така всеки тест, който праща имейл, пращаше истински имейл, директно от домашния ми интернет.
Повечето писма бяха отказани от сървърите на получателите и се върнаха в локалната ми кутия. Това е късметът в цялата история. Dev машина изобщо не бива да може да праща поща на реални хора.
Защо Mac-ът изобщо може да праща имейли
macOS идва с Postfix - истински пощенски сървър. При мен:
postconf -d mail_version
mail_version = 3.2.2
/usr/sbin/sendmail е sendmail програмата на Postfix. PHP функцията mail() подава всяко писмо на нея, защото това е стойността по подразбиране на sendmail_path:
php -i | grep sendmail_path
sendmail_path => /usr/sbin/sendmail -t -i => /usr/sbin/sendmail -t -i
Двата флага:
-t- получателите се вземат от хедърите на писмото (To:,Cc:,Bcc:)-i- ред, на който има само., не се брои за край на писмото
После Postfix сам намира пощенския сървър на получателя и доставя писмото директно по SMTP, от твоя IP адрес. Не трябва нито SMTP акаунт, нито plugin. Просто работи - и точно това е проблемът.
Postfix не върви - докато не тръгне
Ако провериш, Postfix изглежда спрян:
launchctl print system/com.apple.postfix.master | grep -E 'state|program'
state = not running
program = /usr/libexec/postfix/master
Не му вярвай. launchd задачата (/System/Library/LaunchDaemons/com.apple.postfix.master.plist) следи една папка:
"ProgramArguments" => [
0 => "master"
1 => "-e"
2 => "60"
]
"QueueDirectories" => [
0 => "/var/spool/postfix/maildrop"
]
Щом нещо пусне писмо в /var/spool/postfix/maildrop, launchd стартира master процеса на Postfix. Той доставя пощата, а -e 60 го спира 60 секунди по-късно. Така Postfix почти през цялото време е изключен и въпреки това изпраща всеки имейл, който му дадеш.
Затова и mailq обикновено казва това:
postqueue: fatal: Queue report unavailable - mail system is down
Още една настройка, която изглежда безопасна, но не е: macOS слага inet_interfaces = loopback-only. Това значи само, че Postfix не приема връзки от други машини. За изходящата поща не казва нищо.
Postfix не може да се деинсталира, а launchd задачата не може да се махне - и двете са защитени от System Integrity Protection (SIP). Конфигурационният файл /etc/postfix/main.cf обаче се редактира със sudo, и това ни стига.
Конфигурацията на Postfix за две минути
Файловете са в /etc/postfix/:
main.cf- основните настройки, по еднаname = valueна редmaster.cf- кои услуги на Postfix вървят и какmain.cf.defaultиmain.cf.proto- еталонни копия, които идват с macOS
Не редактирай main.cf на ръка. Ползвай postconf, който познава формата:
postconf name1 name2- текущата стойност на тези настройкиpostconf -n- само това, което е изрично зададено вmain.cfpostconf -d name- вградената стойност по подразбиранеpostconf -e 'name = value'- записва настройка вmain.cfpostconf -X name- маха настройка отmain.cfpostconf -# name- коментира настройката, така че тя се връща към стойността по подразбиране
Къде отива едно писмо, решават три настройки:
local_transport- поща за самата машина (например за локалния ти потребител). По подразбиранеlocal:$myhostname.relay_transport- поща за домейните отrelay_domains. Под macOS този списък по подразбиране е празен.default_transport- всичко останало, тоест целият интернет. По подразбиранеsmtp.
Стойността на транспорта е във вида transport:nexthop. Номерът е в error транспорта на Postfix: той изобщо не излиза в мрежата и връща всяко писмо обратно, като ползва текста след двоеточието за причина. Има и discard транспорт, който изхвърля писмото, без да казва на никого. За dev машина предпочитам error - получаваш върнато писмо и виждаш, че нещо се е опитало да прати поща.
Провери какво имаш
postconf default_transport relay_transport
Преди промяната показваше стойностите по подразбиране:
default_transport = smtp
relay_transport = relay
Спри изходящата поща
Една команда:
sudo postconf -e \
'default_transport = error:outgoing mail is disabled on this dev Mac' \
'relay_transport = error:outgoing mail is disabled on this dev Mac'
Провери дали е записано:
postconf default_transport relay_transport
default_transport = error:outgoing mail is disabled on this dev Mac
relay_transport = error:outgoing mail is disabled on this dev Mac
Ако Postfix не върви (нормалното състояние), няма какво да презареждаш - ще прочете main.cf при следващото стартиране. Ако случайно върви, sudo postfix reload прилага промяната веднага.
Локалната доставка продължава да работи. Всичко, адресирано извън Mac-а, веднага се връща в локалната ти кутия заедно с оригиналното писмо, така че нищо не се губи.
Провери опашката за заседнали писма
Писма, които вече не са минали, може да стоят в опашката и да бъдат пробвани отново по-късно. Преброй файловете (трябва sudo, защото папките на опашката са затворени):
sudo find /private/var/spool/postfix/deferred /private/var/spool/postfix/active \
/private/var/spool/postfix/incoming /private/var/spool/postfix/maildrop -type f | wc -l
Докато master процесът е спрян, mailq няма да помогне. postcat чете файловете от опашката директно, а -h показва само хедърите:
sudo find /private/var/spool/postfix/deferred /private/var/spool/postfix/active \
/private/var/spool/postfix/incoming /private/var/spool/postfix/maildrop -type f \
-exec postcat -h {} \; | grep -E '^(From|To|Subject|Date):'
Ако нищо от това не трябва да стига до никого, изтрий всичко:
sudo postsuper -d ALL
ALL трябва да е с главни букви - Postfix го изисква нарочно, за по-сигурно.
Тествай
echo "test body" | /usr/sbin/sendmail you@example.com
Postfix стартира, писмото стига до error транспорта и в локалната ти кутия (/var/mail/$USER) се появява върнато писмо с текста "outgoing mail is disabled on this dev Mac". До получателя не стига нищо.
Не пускай PHP изобщо до Postfix
Блокирането в Postfix покрива целия Mac. За PHP обаче искам имейлите и да се записват като файлове, за да мога да ги отворя и да видя какво точно е пратил кодът ми.
sendmail_path в PHP може да бъде всяка команда, която чете писмото от стандартния вход. Преди да пипаш конфигурацията, пробвай еднократно:
php -d sendmail_path="cat >> $HOME/php_mail.log" \
-r 'var_dump(mail("someone@example.com", "test subject", "test body"));'
bool(true)
А във файла:
To: someone@example.com
Subject: test subject
test body
mail() връща true и нищо не напуска машината. За да остане така, сложи настройката в отделен ini файл в conf.d папката на PHP. С PHP 8.5 от Homebrew на Intel Mac това е /usr/local/etc/php/8.5/conf.d/ - на Apple Silicon Homebrew е под /opt/homebrew. php --ini показва точната папка. Например zzz-mail-catcher.ini:
sendmail_path = "cat >> /Users/YOUR_USER/php_mail.log"
След това php -i | grep sendmail_path трябва да покаже новата стойност.
Един файл за всички имейли стига за проверка от време на време. Ако пращаш много, насочи sendmail_path към малък скрипт, който записва всеки имейл в отделен файл, в папка за всеки ден.
Как да я пуснеш отново
Когато наистина трябва да пратиш нещо от Mac-а, върни стойностите по подразбиране:
sudo postconf -# default_transport relay_transport
-# коментира двата реда в main.cf и Postfix се връща към smtp и relay. Пращаш каквото ти трябва и пускаш отново командата с postconf -e, за да заключиш.
Проверявай след всеки ъпдейт на macOS
postconf -e пише в /etc/postfix/main.cf, така че промяната оцелява след рестарт. С ъпдейт на macOS нещата стоят по-иначе.
При мен main.cf, main.cf.default и main.cf.proto бяха с една и съща дата, а преди да го пипна, main.cf беше със същия размер като main.cf.default. Прилича на това, че инсталаторът на системата сам записва main.cf, така че някой бъдещ ъпдейт може тихо да върне smtp. Още не съм го видял да се случва, но не искам да разбера за него от върнат имейл.
След всеки ъпдейт на macOS пускай:
postconf default_transport
Ако пише smtp, пусни отново командата за заключване. PHP ini файлът е по-защитен: стои в папката на Homebrew, която macOS не пипа. Нова минорна версия на PHP (например 8.6) обаче чете нова conf.d папка, така че файлът трябва да отиде и там.
Бонус: как да се отървеш от "You have mail"
Това съобщение значи, че локалната ти пощенска кутия не е празна. Прочети я с mail, а ако вътре са само върнати писма, които не ти трябват, изпразни я:
: > /var/mail/$USER
Това изтрива писмата, затова първо провери, че няма нещо, което искаш да запазиш.
Следващата ти стъпка
Пусни postconf default_transport на своя Mac още сега. Ако пише smtp, машината ти може да праща истински имейли на всеки, на когото кодът ти реши да пише - включително от тестовете.