Ostatnie kilka dni spędziłem na zamianie wewnętrznego narzędzia do przeszukiwania dokumentów w produkt komercyjny. Samo narzędzie było już gotowe: wyszukiwarka RAG i graf wiedzy po publicznych dokumentach gminy (uchwałach, zarządzeniach, protokołach sesji), czyli coś, z czego informatyk albo sekretarz urzędu skorzysta zamiast ręcznie przekopywać archiwum BIP. Opakowanie produktowe to landing page, widget czatu do rejestrowania leadów i zimny mail do kilku sąsiednich urzędów gmin.
Nic z tego nie było trudne. Trudne było sprawienie, żeby kontakt@ w ogóle wysłało maila.
Konto było bardziej zablokowane, niż wyglądało
Skrzynka domeny działa na Google Workspace, i okazało się, że to konkretne konto ma jakieś dodatkowe ograniczenia, do których źródła nigdy do końca nie dotarłem. Wszystko w konfiguracji wyglądało identycznie jak zwykłe konto Workspace, dopóki nie potrzebowałem, żeby faktycznie wysyłało pocztę programistycznie.
Pierwsza próba: hasła aplikacji. Standardowa sprawa, Google zabił zwykłe logowanie użytkownik/hasło do IMAP/SMTP jeszcze w 2022 roku, a dzisiejsze obejście dla wszystkiego, co nie mówi OAuth, to hasło aplikacji, dostępne po włączeniu weryfikacji dwuetapowej. Więc: włączam 2FA, wchodzę na stronę haseł aplikacji, generuję.
Tylko że strona pokazała: “Ustawienie, którego szukasz, nie jest dostępne na Twoim koncie.” Mimo w pełni włączonej 2FA. Przeszedłem przez zwykłą ścieżkę diagnostyczną: sprawdzić politykę 2-Step Verification w konsoli administracyjnej, potwierdzić że “Allow users to turn on 2-Step Verification” jest włączone, potwierdzić że metoda weryfikacji nie jest zablokowana wyłącznie do kluczy bezpieczeństwa (co z definicji wyłącza hasła aplikacji). Wszystko się zgadzało. Ustawienie i tak nigdy się nie pojawiło.
Okazuje się, że to znane, choć słabo udokumentowane ograniczenie: część kont Workspace po prostu nie udostępnia haseł aplikacji, kropka, niezależnie od żadnego przełącznika administratora. Nie bug, po prostu ściana.
Plan B: zrobić to porządnie przez OAuth2
Dobra. Hasła aplikacji to i tak tylko legacy furtka. Właściwy, wspierany sposób wysyłania poczty jako użytkownik Workspace z backendowego skryptu to Gmail API z kontem serwisowym i domain-wide delegation: założyć projekt w Google Cloud, włączyć Gmail API, utworzyć konto serwisowe, przekazać jego client ID do konsoli administracyjnej z zakresem gmail.send, gotowe.
Zablokowane na pierwszym kroku. Sam dostęp do Google Cloud Platform był dla tego konta wyłączony, schowany za Admin Console → Apps → Additional Google services, a przełącznik, który miał to włączyć z powrotem, też nic nie robi. Bez dostępu do Cloud Platform nie ma konta serwisowego, nie ma Gmail API.
Dwie wspierane ścieżki, dwa ślepe zaułki, zanim wysłałem choć jeden testowy mail.
To, co faktycznie zadziałało
Google Workspace ma usługę SMTP relay, zbudowaną pierwotnie dla skanerów i drukarek wielofunkcyjnych, urządzeń, które mówią SMTP, ale nie mają pojęcia o OAuth ani o oknie logowania. Autoryzuje się nie loginem i hasłem, tylko wpisaniem adresu IP nadawcy na białą listę w konsoli administracyjnej.
Konfiguracja: Admin Console → Apps → Google Workspace → Gmail → Routing → SMTP relay service. Dozwoleni nadawcy ograniczeni do adresów własnej domeny, autoryzacja ustawiona na “przyjmuj pocztę tylko z podanych adresów IP” z dodanym publicznym IP serwera, i “Require SMTP authentication” wyłączone, bo i tak nie było żadnego działającego hasła do podania. Połączenie do smtp-relay.gmail.com:587, STARTTLS, zero logowania, a mail wychodzi jako kontakt@ tej domeny.
Działa. To też naprawdę dziwne uczucie: skonfigurować serwer pocztowy bez jakichkolwiek danych logowania i patrzeć, jak to się udaje.
Dwa przypisy z realnego użycia:
- Poczta wysłana przez relay nigdy nie pojawia się w folderze “Wysłane” tej skrzynki. Za wysyłką nie stoi żadna zalogowana sesja Gmaila, więc Gmail nie ma pod czym tego zaklasyfikować. Obejście to zwykłe BCC do adresu nadawcy przy każdej wiadomości, dzięki temu kopia przynajmniej gdzieś się pojawia.
- Pierwszy prawdziwy mail wysłany w ten sposób został twardo odrzucony przez serwer pocztowy jednego małego urzędu gminy jako spam, z wynikiem na tyle wysokim, że nie było wątpliwości. Domena miała dwa dni i zero rekordu SPF. Dodałem
v=spf1 include:_spf.google.com ~all, potwierdziłem że DKIM już był skonfigurowany przez kreator Workspace, i druga próba doszła bez problemu.
Zimny mail kontra nikt nie pytał
Po rozwiązaniu problemu z pocztą sama wysyłka była niemal antyklimatyczna: krótki, konkretny mail do jedenastu urzędów gmin w jednym regionie, trzy konkretne pytania o to, jak dziś szukają starych uchwał, oferta niezobowiązującej prezentacji na przykładzie dokumentów z jednego roku. Tam gdzie publiczna książka teleadresowa urzędu wskazywała informatyka z imienia i nazwiska, mail szedł bezpośrednio do niego. Tam gdzie nie, szedł na ogólną skrzynkę urzędu.
Zero odpowiedzi do końca tygodnia. Jeden odbity mail (ten z wynikiem spamowym) naprawiony i się nie powtórzył.
Tymczasem, całkowicie niezależnie od tego wszystkiego, oryginalne narzędzie (to, którego nikt nie reklamował, wciąż cicho działające dla gminy, dla której powstało) złapało kilku nowych, realnych odwiedzających w ciągu jedenastu godzin. Konkretne, dobrze sformułowane pytania: mieszkaniec pytający o realną stawkę podatku od nieruchomości, ktoś sprawdzający które uchwały dotyczyły pomocy dla Ukrainy, pytanie o to, które głosowania rady nie były jednogłośne i kto był przeciw. Nikt ich tam nie skierował. Bez kampanii, bez linku, w większości przypadków bez żadnego nagłówka referrera wskazującego skądkolwiek.
Jedenaście maili dostało ciszę. Narzędzie, którego nikt nie sprzedawał, dostało prawdziwe pytania od prawdziwych ludzi. Nie mam z tego jakiejś eleganckiej lekcji poza tym, że to lepszy sygnał niż jakikolwiek wskaźnik odpowiedzi na maile.
Bug spowodowany przez monitoring
Po drodze zepsuło się jeszcze jedno, i to w sposób, który z perspektywy czasu jest niemal zabawny. Naprawiając zupełnie inną lukę (reklamowaną funkcję “synchronizacja co noc”, która w rzeczywistości nigdy nie była zaplanowana) dodałem brakujące zadanie crona razem z małą tabelą statusu i endpointem HTTP, który zewnętrzny harmonogram mógł odpytać, żeby potwierdzić że synchronizacja faktycznie się wykonała.
Pierwsze zaplanowane uruchomienie padło. Nie przez logikę samej synchronizacji. rsync skopiował skrypt synchronizacji z lokalnej kopii roboczej, która po drodze zgubiła bit wykonywalności, cicho zdejmując ten sam bit z kopii na serwerze. Cron próbował go uruchomić, dostał Permission denied, a własna obsługa błędów skryptu nigdy nie dostała szansy zadziałać, bo powłoka nie zdążyła nawet zacząć go wykonywać. Monitoring zbudowany żeby łapać błędy nie złapał tego bezpośrednio. Złapał to przez to, że milczał tam, gdzie powinien zaraportować sukces, co w praktyce jest tym samym, tylko o jeden krok dalej.
Naprawiłem uprawnienia, tym razem zacommitowałem tryb pliku, żeby git faktycznie go pilnował, i poszedłem dalej.
Nic z tego nie zajęło tyle, ile brzmi. Wszystko zajęło dłużej, niż powinno.
