From b616c18c58d9c2ca3a397ec12eb299048234970a Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 22 Mar 2026 16:28:29 +0700 Subject: [PATCH] feat: add docker-mailserver for self-hosted outbound SMTP Adds docker-mailserver (SMTP_ONLY mode) to the tools stack so Outline can send magic-link emails without depending on an external SMTP provider. Changes: - docker-compose.yml.j2: add mailserver service + mail-internal network outline gets mail-internal network to reach mailserver - env.j2: point Outline SMTP at local mailserver:587 with noreply account - defaults/main.yml: add mailserver_image (v14) - tasks/main.yml: create mailserver dirs, wait for postfix ready, idempotent account creation, DKIM key generation + DNS instructions - inventory/group_vars/all/main.yml: add mailserver_noreply_password alias - vault.yml: add vault_mailserver_noreply_password After deploy, Ansible will print DKIM/SPF/DMARC DNS records to add to Cloudflare. Co-Authored-By: Claude Sonnet 4.6 --- inventory/group_vars/all/main.yml | 1 + inventory/group_vars/all/vault.yml | 214 ++++++++++---------- roles/tools/defaults/main.yml | 8 +- roles/tools/tasks/main.yml | 65 ++++++ roles/tools/templates/docker-compose.yml.j2 | 36 ++++ roles/tools/templates/env.j2 | 15 +- 6 files changed, 219 insertions(+), 120 deletions(-) diff --git a/inventory/group_vars/all/main.yml b/inventory/group_vars/all/main.yml index 6a94246..2d8ff1a 100644 --- a/inventory/group_vars/all/main.yml +++ b/inventory/group_vars/all/main.yml @@ -47,6 +47,7 @@ outline_utils_secret: "{{ vault_outline_utils_secret }}" outline_db_password: "{{ vault_outline_db_password }}" n8n_encryption_key: "{{ vault_n8n_encryption_key }}" n8n_jwt_secret: "{{ vault_n8n_jwt_secret }}" +mailserver_noreply_password: "{{ vault_mailserver_noreply_password }}" # Server IPs (used for cross-server Traefik routing) ip_main: "87.249.49.32" ip_tools: "85.193.83.9" diff --git a/inventory/group_vars/all/vault.yml b/inventory/group_vars/all/vault.yml index f6d5fb0..6405e9c 100644 --- a/inventory/group_vars/all/vault.yml +++ b/inventory/group_vars/all/vault.yml @@ -1,106 +1,110 @@ $ANSIBLE_VAULT;1.1;AES256 -31636461643831333531333865363466633066313930383165643866653465326136373363643830 -3133373732303438613337353435333764643761356534300a376331636161373664393634376265 -66373962303534383636663137306531346564303533613235613935613632373338343630306339 -6133633166396138360a356463323462643266336132303365313363343062346265343961363431 -32373838326536396630643636356461663539313131376538656261376166363062343133666262 -36386430353963323435343937613539326537333463326538613464356631616362396636366166 -34373333363232326639303563343965373733646264383263666266393265313732303036613538 -31356633376631303533613462643836323462333930306331666563326536303166336339356633 -39393563346139666533376535633166643336646166663962623730346430663363653537623931 -38663466616661393532326136636230633363363164626239616438353737353830653730613936 -31373037656331356563333031393861633435333364653165633237323532353962383066636462 -64646666323235323432336135333466613265363738643630346539636465383833366139303634 -66373961663139396464393764353130666331623836386338343862323732346432643030626238 -34336563356266333434353035363034363532656532303730363432633435383034393666393439 -38626235646365366536303231633635353937666535666630323938303039323666653730383731 -36313434616466353538626236333965323861376665383836343136376365303730336130313134 -63613335633837313130626339626136646334333931666364376535356163363864656265336433 -39333934333637323534343236366432333337373135333762333036343365383438326636613033 -33373562373661616237323636343064623831363637313338343631343739363230623164656466 -64323761383533643136393539613331353261396663316236613336663738636463633132653534 -38653634393965383765656431343830386163333433323961353764316565326638303434656161 -31383735356238643761323733353138313934386137633530636430643934363136373666653862 -32383634396263326563656364313461636530366336336264326538643830396262326364633437 -32656566363763396532316334333331353334316662636330396134613363663034303162323133 -62363235303532663536383236326263646366333732333638363631326131306233623035306663 -62333035623566636235663963316430303932313166313439396135373033653364376263633937 -33616233663333316438333834313831383232666531636336666634316361386538323364366532 -62343335333836646231386164333533366638626437333531303263656262356566363462363561 -39326430366363623139393461366265613833393638626337636632376364326461313934653939 -37336136633161393439383238666537643530616535333861323635386433356463646266326666 -36343735316464306238376439663035356331366433326134633631386531313638663265363532 -63336366646533333662323739316561393537383033346538363763303333643538363264656230 -32653335643039613166386463326662393336363532313064633862313761623265333665356132 -33646131336561393333363562653566663062353064383834663430613365323234306336343636 -64383661323761326534326564313465323333623631643962313163316533343361323836346461 -62623839343530366236323338626134613761396638303230336134303537306338623364376234 -35653162376533623636616538336337646330373765366133326134653334313032343733336436 -37636462333239326632643830643463623534306337333531646462376434343163353030643430 -61343933373339376639316330363139653234636635303739633239613566613634666339643832 -62616266346232353737376139626233363335363432323338353935343737663938656630653238 -66313231353438346431333533366664313661663330623466316663366337373531636663643632 -30353863383635353764633436613964646631373730313232346664343463653163323030643266 -66646634373862633361656464646539313433613363313861376462653731383663386662643062 -64333164623130666233366536363761343138633935363164653066373837666339323165333362 -34616239373062623332383562653961343963386431643665306230656664613766333733353834 -62396430616564303830326531643336363631363234626635636133313435626539646433613361 -64653437363036363461626364393336323336323634393239626631353430646466343164623465 -35623130346663376336336530366161623361663034373433313430636236616661316666346665 -64303137323362313938363530316462363666396138663764306561393434636331653137343063 -32306136313263373432336162363737306166663666646264666438363537346363613030653733 -32653866306134623366616231393234653338653265636137343136356430633236623063306333 -61373437393831323830393663663832653939616437373963383934336266373434323133336239 -62626334323266343739643932663961616130386435636230613165366232343138346435353739 -65393464326531323034333864646661346465633537373632613431663035643536623965336334 -65363431343362613639353361313365643438376230353934646466643239316435396266326231 -37633362326637323265343437356138633461303031323533613963396536323164666461353034 -65373763626362313766623030316164303964633830613337613139613163396439373430633738 -38663839646361663164343261656536356337613531393136636134383363333366633866623664 -65373364363734666366393462323362326564333431393131303264646536663539363066373938 -62643162346661643333313134623865666666643736313235346638353363346435656439353335 -36326534656338383666316537303734343462303136653264393538303038373131616664363434 -38376131333734303039396232343265303239333330623361666462363261366365626461613034 -35616361373631366233623464666261626135353064323635363766356331303864396433323865 -32366365623730353033373039626235306166323035393331363839356162323363326366613065 -31303630363763663561326238663533633537376561303935363734306234343063393863353738 -38346365373331383963643430633164313632323133323838643436646137313361653631376430 -38323964383264653237343334386638366463393461653239303734656537616261383361376464 -32666266363039316365303563346133356632653366303661626331313763306538386533336137 -62373336366466306634323539623439336632333036313361346164316666396333623832393732 -32393261393865353362383434326137633963613265343136663834666366633136646335653064 -64306162656131636439316636353463383638323938306662336134323035656262386462616339 -65373431616332613234353230653365313935646430363435616166323238643533313932356134 -38376535373739616338303065623863303030613464633330316337313238623234643862356537 -33313630663163626331623862373861613435393566633735636436643939323436616366353633 -36623235386434613163303430366366633830376565383632613565393931326531323566353531 -65653032383139666538373063383337306235393334383164626337623564663939653933363830 -35663630636565636639653662633362643562356638643864383030316565636564306362336539 -34376435353434393034613365613533346339353935353233663231623235623863623665616135 -65313263643965343833373063616238313465373534613330613035346337343832386136343736 -61333637393065653331353737663737376630383036646531353432653434363436323866663331 -35656463663830343864643064313931633763373762613233663038653835643932616137646163 -31356631346530653461343262303836306463393838666236613834623737666666643035653433 -30366138663935613336303931396439343762663566366563303834306465323132363831306337 -31363134383263663832383838356332326663663562646536313538653961623233636663336665 -66373663356335333161336231313564633834353137643763616261333564656561346130623339 -31366435303636646362653034633634636433353032353338373166316336653132373934313630 -62656362303631636435303139383766383931626133336565633262323361326337666362353538 -66343532353532313964343330323666633231313765336535376431653836343866633334336437 -62343538623663373039343134326235613130313534613537386466353130653764653435663666 -34363532613635316538363964643435653062643736396362626534363064386536653963383633 -32326335613336363332313036353338303738363865346362316339356166333131626339303663 -39613531653763613638343839313162636239383030643134373035616361313936386333656633 -36646238316566386238333033636439373633393034663466653635616237616566383466613465 -39383461663131633461313261613339613633303935636639303337313965306637306538643036 -33643665616135643234393739333532356462303263363065613461623631643030666331316562 -36313739356339623465353564363537666335313132663464346665383936346362643565326237 -66336239343735336433636466313163353332333061646638616534656131313437383561653561 -31613861386363326136303833343966313862353234373762356630623335613866666534376533 -63303034333630623164373064383730356136373961646465663766303031303932343664396163 -32646335303562313565386434353036646463343336646665333334336339303465613530373234 -31663461643664313136396633613532363766363635363132323432643161373130333635646335 -33376438323132656335633862653630356534396339303739303538383837343031386232346137 -63366530313661313731326664363936373163383066656635616137653132613134636635346531 -36353130333461323230626239636234326236643366383265626535356130363538653735626337 -30386633373366383339346431656239383136313838363032333663366238623835 +64646131626662663937643431386162343036353133623631303531353735653862353835303438 +6637363563353433326235383231633363326533653634660a316436323933373162316463343063 +32656364646331363232376632353737303330356633336261383561333165623664666130636335 +3734366139333039360a353034636336313362383734623264616439303734323763343535616661 +34336136663561336334666430376566353865363837613834326336383563373131666562663238 +37636638613966373162633565366361613965636232393266373039313931323361303130356463 +35653132336232393934626631366137346366373635636633346530633134336264663761316564 +31626230363562343035323334373364316333306162353565356461373938653235303562383134 +35623561313165366364613036643264303738613038316163636164663833333635356561646438 +37336265363835326363313964333033636537366232313336633932386261373031663338343334 +35336664313836336434666237663539386337656461396264613562313536653463393231306463 +33633937356564643235383562346461373563656435626139643530326236313030396638323365 +32393531363064343030333461343739633363616665376262663831303630653034633563313834 +31373938313962386634383937393031633263383536613964373562393438643332656635306632 +37613634666533646462626661646463326164323361306337316463653536323938373866633834 +32336338333465306131396261383933313538323163636332323962316162306234323263336239 +35663965626662633134363066323831623630326365376432616464326532636435313965613561 +37343031373764326639646365663663633765393230323466343539303034323939336330383836 +30343135313466396136326461333131336232656530656264383563363166323364636338653165 +65663963373365646661616661393766653964663135643431646535333335353162623866326663 +37356534316437653130316531323065636335613931366563626639393439623734653865326636 +66356565303637613639626634313464333033373231656432343765363332666232656162386336 +66666563356465363263373936366238643535633364663263383337396565313337353537623061 +63346536663765623535363037323536633730306135636566663866663763613137653033666435 +32653661333865613632323339633631643637663438333065336666666562663535633135316639 +31333563663130396164386535373335613364643966623164353639393536343866666437613137 +65636564343164306466333333333930316337623161363163656365653465313135393233363562 +39396437363239643233653938343030386539666332666130303764373138633236326534636638 +62336630666331646365643039386665343964346336343837336639653864653331323438323436 +38376361613161376133323066633930333165343939376135376531363939303139326161333030 +39666366613665346330303863666634393366396439656133653038623466383838343833626365 +35346666316265393561303563313333636337323334316534303637303133303639613838346633 +31336437633733646237646432653462323462363165393665363564636566666161333436363163 +33323439636138343064613530633132616264626338336132356234313863393934646133313933 +36626133613862616130666464386361393265333064373538353435376231346134343066633939 +61346561633933623336653462623764383938616466643138346164303863303734626162383431 +37623036613032353431646332623735393265656533373536313065393665663433376235663361 +66373934646133623264363433643437316261343739316164363065653263373539383462623439 +64663733336535613736653731623137613137653962363262303836306162616339626430353662 +65653739336136623934333837663966663838393164353933333039306339323131613535636333 +39383836373061353164323337643135383164383434333465643331346230326633643965366531 +34316635636464616162666234616164393636616165326535336435373633323036313131653964 +62666363313836633765393862396235633434353835373961393164333538313965623266333839 +64303363373862613266383262643966613738376434343263346538623937346235616534373534 +64316235386231373736663934636335343236316563626161323437323831653236383139313936 +35666664616632643030643536323336316535646130633762333166353632613139363964386462 +62323764333930653534613834313964323832336161333538626531383631363238316634336131 +39616130626338343861613337383564636265383032313833366264333231393633666432373234 +66646137303336643937313938326236643765636330613064336632343631623631613831336332 +38333364396338613065643939346161336431313261353761616461393161643534623364393235 +36306337396363336231343230323536623933346634363037333135353530643665663034633134 +35393839343865373561623561353739396261343833373137616663386164326465323234393938 +63306535623331366462383961373639366631353738313534656230376365323436353165346663 +33663939353463373866366230366463633733623266613135343765393431663436666466363831 +38323561663565363862633037303237613231333435646564383730623462653333326633316334 +65643661363036613837303366323261633563313338323663303064623937653837623861376636 +61323437656139363461386535396436386564316662396361396133663834353064376634326565 +37393433656532653638303433376332643536666635383334666161393664616265613430313337 +31373936646430383132613535343562643638363666323162386638643263613436643562333631 +64363034323232303161396536396136373163663035393238346462616539366637356233386363 +63343062373062666363393663396633643331626561643133303866313864353831323162623962 +61356237363235653038663132623263333565333065363361363362303431623462343361326638 +34316139313635366435393365643136303264626264643961643938633238346161616339653535 +37656238623762363564626365316361303730303936626637323066343537656231326136666134 +38326538363230393533383439666165353037636162626538326536616635393831313031613133 +38333137393361383931336437363434303232633733623832343534343039643038373932656361 +35316464663936306632326632323462376435633234386261383338663034336132336338353836 +64646433383337333662623861303632653636333332396466613439643565326331376535333466 +30333661366661303531623437393265353736646464653535336162383737333832383966306366 +38363136663732383335303966636161653833313338393137373463303465326239666161303939 +62363761646539383462313464353332356634333762383836366334386236363062323131316436 +65313465373038653237633963316138353339366666343133316366396236343831313035653665 +62383135383763346336393534396236303832336361616264373838666331363636323434363961 +63363539313164393035333361373765626532383735643535316364646134656632353165646138 +33386638366661396538366465666236656166613031653338396232336239393631353435373564 +35333637313166353035626465653235396133303762623630333963313063613963363236663964 +62623437326535643138346536333637643839343964306364313532383932373834653632383839 +65303736353362303839623830653939316139376232373531313165346266306130633566393732 +32636662343931383235646165626632636330663634643238653562373864346537333639656235 +34653864393338383964303861393866333333666164376137386664346638356566386430353663 +61363139353861633665636565393966663535333731396264636531303233663865366538656362 +66333234613465663332663765386461376239396161383230363763616236303236636565616664 +36306539323433303132333733653039666666636463646535613737653934383161656436636664 +65346634633430666165316631636334333537393565623131636338623333313033336665346132 +39313065653932643132333536353737326663616133326130623834396530663635616339363162 +64316163376266653463333637373939376463656232666461306163376230353336633732653965 +64386139356336353265343233643761313261666463363738353631366233636463303931396162 +34336439346139363832323333646365356630613930366431653766653633343634363066363639 +61656535333662363865633130663431386633363336663730363934306435383838343933633231 +30303138303237666236336461353164396336346236626666383065386365636236306435343233 +63396436656364396662326364633636616366363531336363306333333831383339346232323261 +33616164356464636165376235353835613966333730663833636530646430383134333437333333 +39336664643937303462356330323066343633306164373064666634303862326261666238653938 +65346266303665636464663564653064663839663261666665336635613934333933326666306262 +34333861626135386239656333323037663463326531663966313266616637636435383862396237 +37316231666662343765326638313335353032636236633263646539646636636432343661326463 +62393336646638646531313837393935356465613136373330363131663130323632626261646636 +35656166306437643631663261656166313232373339666265393033656631366532346163333038 +38306431323264666461646661646537363235386434326362346239356466663137363135383537 +61613363646235376633303232316564373731666537373032633937393832393935313039656464 +66623236666564313665343461343337326233343131623331636635363131396339383266633461 +63323063363138353266643433616439333433353032313861363932336336656536343839626536 +32316434376536316232666430653561313661656264383665386663356562303132393666333064 +36306236363137383762626431373937313933653735326131383038636263396431393037373235 +33376234366265343966356462306134663734363837323464313137653863643436393437653065 +61643935633862303330653666376261326537386564396330636561656230633630393466666364 +31303134636262616461393465616635663061306330316139623462306266346464303034323066 +37333830323335303065626436343837636462616263663434303563333430386663393465303862 +3261 diff --git a/roles/tools/defaults/main.yml b/roles/tools/defaults/main.yml index a3fb70d..acac4eb 100644 --- a/roles/tools/defaults/main.yml +++ b/roles/tools/defaults/main.yml @@ -4,10 +4,4 @@ outline_image: "outlinewiki/outline:0.80.2" outline_db_image: "postgres:15-alpine" outline_redis_image: "redis:7-alpine" n8n_image: "n8nio/n8n:1.89.2" # https://hub.docker.com/r/n8nio/n8n/tags - -# SMTP for Outline magic-link auth (override in vault) -outline_smtp_host: "smtp.csrx.ru" -outline_smtp_port: 587 -outline_smtp_from: "noreply@csrx.ru" -outline_smtp_username: "" -outline_smtp_password: "" +mailserver_image: "ghcr.io/docker-mailserver/docker-mailserver:14" # https://github.com/docker-mailserver/docker-mailserver/releases diff --git a/roles/tools/tasks/main.yml b/roles/tools/tasks/main.yml index 21120d8..8bf6b99 100644 --- a/roles/tools/tasks/main.yml +++ b/roles/tools/tasks/main.yml @@ -7,6 +7,19 @@ group: "{{ deploy_group }}" mode: "0750" +- name: Create mailserver directories + ansible.builtin.file: + path: "{{ tools_root }}/mailserver/{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + loop: + - mail-data + - mail-state + - mail-logs + - config + - name: Deploy docker-compose.yml ansible.builtin.template: src: docker-compose.yml.j2 @@ -32,9 +45,61 @@ - "{{ outline_db_image }}" - "{{ outline_redis_image }}" - "{{ n8n_image }}" + - "{{ mailserver_image }}" - name: Start tools stack community.docker.docker_compose_v2: project_src: "{{ tools_root }}" state: present pull: missing + +- name: Wait for mailserver to be ready + ansible.builtin.command: docker exec mailserver postfix status + register: postfix_status + changed_when: false + retries: 12 + delay: 10 + until: postfix_status.rc == 0 + +- name: Check if noreply mail account exists + ansible.builtin.command: > + docker exec mailserver setup email list + register: mail_accounts + changed_when: false + +- name: Create noreply mail account + ansible.builtin.command: > + docker exec mailserver setup email add noreply@{{ domain_base }} {{ mailserver_noreply_password }} + when: "'noreply@' + domain_base not in mail_accounts.stdout" + +- name: Check if DKIM key exists + ansible.builtin.stat: + path: "{{ tools_root }}/mailserver/config/rspamd/dkim/{{ domain_base }}.private" + register: dkim_key + +- name: Generate DKIM key + ansible.builtin.command: > + docker exec mailserver setup config dkim domain {{ domain_base }} + when: not dkim_key.stat.exists + register: dkim_generated + +- name: Show DKIM DNS record + ansible.builtin.command: > + cat {{ tools_root }}/mailserver/config/rspamd/dkim/{{ domain_base }}.txt + when: dkim_generated is changed + register: dkim_record + +- name: Print DKIM DNS instructions + ansible.builtin.debug: + msg: | + ══════════════════════════════════════════════════════════════════ + DKIM key generated! Add this TXT record to Cloudflare DNS: + {{ dkim_record.stdout }} + + Also add SPF record: + Type: TXT Name: @ Value: v=spf1 ip4:{{ ip_tools }} ~all + + And DMARC record: + Type: TXT Name: _dmarc Value: v=DMARC1; p=none; rua=mailto:admin@{{ domain_base }} + ══════════════════════════════════════════════════════════════════ + when: dkim_generated is changed diff --git a/roles/tools/templates/docker-compose.yml.j2 b/roles/tools/templates/docker-compose.yml.j2 index 6191951..17f0fed 100644 --- a/roles/tools/templates/docker-compose.yml.j2 +++ b/roles/tools/templates/docker-compose.yml.j2 @@ -12,6 +12,9 @@ networks: n8n-internal: driver: bridge internal: true + mail-internal: + driver: bridge + internal: true volumes: outline_db_data: @@ -28,6 +31,7 @@ services: env_file: .env networks: - outline-internal + - mail-internal # send mail via mailserver - front # needed for host port binding ports: # Exposed only to main Traefik (access controlled by UFW) @@ -126,3 +130,35 @@ services: options: max-size: "10m" max-file: "3" + + # ── Mail server (outbound SMTP for Outline magic links) ────────────────────── + mailserver: + image: {{ mailserver_image }} + container_name: mailserver + hostname: mail + domainname: {{ domain_base }} + restart: unless-stopped + networks: + - mail-internal + environment: + - SMTP_ONLY=1 # no IMAP/POP3, sending only + - ENABLE_RSPAMD=0 # lightweight: skip spam filter + - ENABLE_CLAMAV=0 # no antivirus needed for outbound-only + - ENABLE_FAIL2BAN=0 # host fail2ban already handles this + - POSTFIX_INET_PROTOCOLS=ipv4 + - LOG_LEVEL=warn + - OVERRIDE_HOSTNAME=mail.{{ domain_base }} + - POSTMASTER_ADDRESS=admin@{{ domain_base }} + volumes: + - {{ tools_root }}/mailserver/mail-data:/var/mail + - {{ tools_root }}/mailserver/mail-state:/var/mail-state + - {{ tools_root }}/mailserver/mail-logs:/var/log/mail + - {{ tools_root }}/mailserver/config:/tmp/docker-mailserver + stop_grace_period: 1m + cap_add: + - NET_ADMIN + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/roles/tools/templates/env.j2 b/roles/tools/templates/env.j2 index e45e9e1..75e323a 100644 --- a/roles/tools/templates/env.j2 +++ b/roles/tools/templates/env.j2 @@ -27,14 +27,13 @@ FILE_STORAGE=s3 # Auth — local accounts (can add OIDC/Authelia later) AUTH_PROVIDERS=email -# SMTP (required to enable email magic-link auth) -SMTP_HOST={{ outline_smtp_host }} -SMTP_PORT={{ outline_smtp_port | default(587) }} -SMTP_FROM_EMAIL={{ outline_smtp_from }} -{% if outline_smtp_username is defined and outline_smtp_username %} -SMTP_USERNAME={{ outline_smtp_username }} -SMTP_PASSWORD={{ outline_smtp_password }} -{% endif %} +# SMTP — local docker-mailserver container (same Docker network, port 587 with auth) +SMTP_HOST=mailserver +SMTP_PORT=587 +SMTP_USERNAME=noreply@{{ domain_base }} +SMTP_PASSWORD={{ mailserver_noreply_password }} +SMTP_FROM_EMAIL=noreply@{{ domain_base }} +SMTP_SECURE=false # Outline DB password (used in docker-compose) OUTLINE_DB_PASSWORD={{ outline_db_password }}