From 5a0b5e3c5c63ca85dc0d964f3374f556f81053e2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Jun 2021 11:09:18 +0000 Subject: [PATCH] deploy: a0f4a375124b3b3402d5639a7d422501c2011b97 --- v10.0/assets/css/customizations.css | 84 + v10.0/assets/fonts/external-link.woff | Bin 0 -> 1008 bytes v10.0/assets/img/bg-water.webp | Bin 0 -> 92840 bytes .../assets/javascripts/bundle.34eae1b6.min.js | 32 + .../workers/search.d351de03.min.js | 61 + v10.0/assets/logo/dmo-logo-white.min.svg | 1 + v10.0/assets/logo/dmo-logo.min.svg | 1 + v10.0/assets/logo/favicon-32x32.png | Bin 0 -> 1130 bytes .../assets/stylesheets/main.875de78c.min.css | 2 + .../stylesheets/palette.f1a3b89f.min.css | 2 + v10.0/config/advanced/auth-ldap/index.html | 1703 ++++++++ .../advanced/full-text-search/index.html | 1441 +++++++ v10.0/config/advanced/ipv6/index.html | 1430 +++++++ v10.0/config/advanced/kubernetes/index.html | 1982 +++++++++ .../config/advanced/mail-fetchmail/index.html | 1526 +++++++ .../mail-forwarding/aws-ses/index.html | 1326 ++++++ .../mail-forwarding/relay-hosts/index.html | 1532 +++++++ v10.0/config/advanced/mail-sieve/index.html | 1448 +++++++ .../maintenance/update-and-cleanup/index.html | 1407 +++++++ .../advanced/optional-config/index.html | 1420 +++++++ .../override-defaults/dovecot/index.html | 1435 +++++++ .../override-defaults/postfix/index.html | 1327 ++++++ .../override-defaults/user-patches/index.html | 1337 ++++++ .../best-practices/autodiscover/index.html | 1309 ++++++ v10.0/config/best-practices/dkim/index.html | 1513 +++++++ v10.0/config/best-practices/dmarc/index.html | 1386 ++++++ v10.0/config/best-practices/spf/index.html | 1423 +++++++ v10.0/config/environment/index.html | 3748 +++++++++++++++++ v10.0/config/pop3/index.html | 1316 ++++++ v10.0/config/security/fail2ban/index.html | 1324 ++++++ v10.0/config/security/mail_crypt/index.html | 1433 +++++++ v10.0/config/security/ssl/index.html | 2112 ++++++++++ .../understanding-the-ports/index.html | 1647 ++++++++ v10.0/config/setup.sh/index.html | 1485 +++++++ .../troubleshooting/debugging/index.html | 1475 +++++++ .../user-management/accounts/index.html | 1426 +++++++ .../config/user-management/aliases/index.html | 1406 +++++++ v10.0/contributing/coding-style/index.html | 1609 +++++++ v10.0/contributing/documentation/index.html | 1292 ++++++ .../issues-and-pull-requests/index.html | 1439 +++++++ v10.0/contributing/tests/index.html | 1306 ++++++ .../tutorials/basic-installation/index.html | 1514 +++++++ .../mailserver-behind-proxy/index.html | 1517 +++++++ .../index.html | 1467 +++++++ v10.0/faq/index.html | 2004 +++++++++ v10.0/favicon.ico | Bin 0 -> 15086 bytes v10.0/index.html | 1387 ++++++ v10.0/introduction/index.html | 1717 ++++++++ v10.0/search/search_index.json | 1 + v10.0/sitemap.xml | 151 + 50 files changed, 57904 insertions(+) create mode 100644 v10.0/assets/css/customizations.css create mode 100644 v10.0/assets/fonts/external-link.woff create mode 100644 v10.0/assets/img/bg-water.webp create mode 100644 v10.0/assets/javascripts/bundle.34eae1b6.min.js create mode 100644 v10.0/assets/javascripts/workers/search.d351de03.min.js create mode 100644 v10.0/assets/logo/dmo-logo-white.min.svg create mode 100644 v10.0/assets/logo/dmo-logo.min.svg create mode 100644 v10.0/assets/logo/favicon-32x32.png create mode 100644 v10.0/assets/stylesheets/main.875de78c.min.css create mode 100644 v10.0/assets/stylesheets/palette.f1a3b89f.min.css create mode 100644 v10.0/config/advanced/auth-ldap/index.html create mode 100644 v10.0/config/advanced/full-text-search/index.html create mode 100644 v10.0/config/advanced/ipv6/index.html create mode 100644 v10.0/config/advanced/kubernetes/index.html create mode 100644 v10.0/config/advanced/mail-fetchmail/index.html create mode 100644 v10.0/config/advanced/mail-forwarding/aws-ses/index.html create mode 100644 v10.0/config/advanced/mail-forwarding/relay-hosts/index.html create mode 100644 v10.0/config/advanced/mail-sieve/index.html create mode 100644 v10.0/config/advanced/maintenance/update-and-cleanup/index.html create mode 100644 v10.0/config/advanced/optional-config/index.html create mode 100644 v10.0/config/advanced/override-defaults/dovecot/index.html create mode 100644 v10.0/config/advanced/override-defaults/postfix/index.html create mode 100644 v10.0/config/advanced/override-defaults/user-patches/index.html create mode 100644 v10.0/config/best-practices/autodiscover/index.html create mode 100644 v10.0/config/best-practices/dkim/index.html create mode 100644 v10.0/config/best-practices/dmarc/index.html create mode 100644 v10.0/config/best-practices/spf/index.html create mode 100644 v10.0/config/environment/index.html create mode 100644 v10.0/config/pop3/index.html create mode 100644 v10.0/config/security/fail2ban/index.html create mode 100644 v10.0/config/security/mail_crypt/index.html create mode 100644 v10.0/config/security/ssl/index.html create mode 100644 v10.0/config/security/understanding-the-ports/index.html create mode 100644 v10.0/config/setup.sh/index.html create mode 100644 v10.0/config/troubleshooting/debugging/index.html create mode 100644 v10.0/config/user-management/accounts/index.html create mode 100644 v10.0/config/user-management/aliases/index.html create mode 100644 v10.0/contributing/coding-style/index.html create mode 100644 v10.0/contributing/documentation/index.html create mode 100644 v10.0/contributing/issues-and-pull-requests/index.html create mode 100644 v10.0/contributing/tests/index.html create mode 100644 v10.0/examples/tutorials/basic-installation/index.html create mode 100644 v10.0/examples/tutorials/mailserver-behind-proxy/index.html create mode 100644 v10.0/examples/uses-cases/forward-only-mailserver-with-ldap-authentication/index.html create mode 100644 v10.0/faq/index.html create mode 100644 v10.0/favicon.ico create mode 100644 v10.0/index.html create mode 100644 v10.0/introduction/index.html create mode 100644 v10.0/search/search_index.json create mode 100644 v10.0/sitemap.xml diff --git a/v10.0/assets/css/customizations.css b/v10.0/assets/css/customizations.css new file mode 100644 index 00000000..53fee32d --- /dev/null +++ b/v10.0/assets/css/customizations.css @@ -0,0 +1,84 @@ +/* This file adds our styling additions / fixes to maintain. */ + +/* ============================================================================================================= */ + +/* External Link icon feature. Rejected from upstreaming to `mkdocs-material`. +Alternative solution using SVG icon here (Broken on Chrome?): https://github.com/squidfunk/mkdocs-material/issues/2318#issuecomment-789461149 +Tab or Nav sidebar with non-relative links will prepend an icon (font glyph) +If you want to append instead, switch `::before` to `::after`. +*/ +/* reference the icon font to use */ +@font-face { + font-family: 'external-link'; + src: url('../fonts/external-link.woff') format('woff'); +} + +/* Matches the two nav link classes that start with `http` `href` values, regular docs pages use relative URLs instead. */ +.md-tabs__link[href^="http"]::before, .md-nav__link[href^="http"]::before { + display: inline-block; /* treat similar to text */ + font-family: 'external-link'; + content:'\0041'; /* represents "A" which our font renders as an icon instead of the "A" glyph */ + font-size: 80%; /* icon is a little too big by default, scale it down */ +} + +/* ============================================================================================================= */ + +/* UI Improvement: Header bar (top of page) adjustments - Increase scale of logo and adjust white-space */ +/* Make the logo larger without impacting other header components */ +.md-header__button.md-logo > img { transform: scale(180%); margin-left: 0.4rem; } +/* Reduce the white-space between the Logo and Title components */ +.md-header__title { margin-left: 0.3rem; } + +/* ============================================================================================================= */ + +/* UI Improvement: Add light colour bg for the version selector, with some rounded corners */ +.md-version__current { + background-color: rgb(255,255,255,0.18); /* white with 18% opacity */ + padding: 5px; + border-radius: 3px; +} + +/* ============================================================================================================= */ + +/* + UX Bugfix for permalink affecting typography in headings. + Upstream will not fix: https://github.com/squidfunk/mkdocs-material/issues/2369 +*/ + +/* Headings are configured to be links (instead of only the permalink symbol), removes the link colour */ +div.md-content article.md-content__inner a.toclink { + color: currentColor; +} + +/* Instead of a permalink symbol at the end of heading text, use a border line on the left spanning height of heading */ +/* Includes optional background fill with rounded right-side corners, and restores inline code style */ +/* NOTE: Headings with markdown links embedded disrupt the bg fill style, as they're not children of `a.toclink` element */ +div.md-content article.md-content__inner a.toclink { + display: inline-block; /* Enables multi-line support for both border and bg color */ + border-left: .2rem solid transparent; /* transparent placeholder to avoid heading shift during reveal transition */ + margin-left: -0.6rem; /* Offset heading to the left */ + padding-left: 0.4rem; /* Push heading back to original position, margin-left - border-left widths */ + transition: background-color 200ms,border-left 200ms; + + /* Only relevant if using background highlight style */ + border-radius: 0 0.25rem 0.25rem 0; + padding-right: 0.4rem; +} + +div.md-content article.md-content__inner a.toclink:hover, +div.md-content article.md-content__inner :target > a.toclink { + border-left: .2rem solid #448aff; /* highlight line on the left */ + background-color: #b3dbff6e; /* background highlight fill */ + transition: background-color 200ms,border-left 200ms; +} + +/* Upstream overrides some of the `code` element styles for headings, restore them */ +div.md-content article.md-content__inner a.toclink code { + padding: 0 0.3em; /* padding to the left and right, not top and bottom */ + border-radius: 0.2rem; /* 0.1rem of original style bit too small */ + background-color: var(--md-code-bg-color); +} + +.highlight.no-copy .md-clipboard { display: none; } + +/* ============================================================================================================= */ diff --git a/v10.0/assets/fonts/external-link.woff b/v10.0/assets/fonts/external-link.woff new file mode 100644 index 0000000000000000000000000000000000000000..6e888e08eb8c9d68a544cb12a2a6a303ad4783cb GIT binary patch literal 1008 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≧}p-0pGLM7br_KFzf;UZ6LFABQ&7 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 DMbEF^ literal 0 HcmV?d00001 diff --git a/v10.0/assets/javascripts/bundle.34eae1b6.min.js b/v10.0/assets/javascripts/bundle.34eae1b6.min.js new file mode 100644 index 00000000..066864cc --- /dev/null +++ b/v10.0/assets/javascripts/bundle.34eae1b6.min.js @@ -0,0 +1,32 @@ +(()=>{var zi=Object.create,dt=Object.defineProperty;var qi=Object.getOwnPropertyDescriptor;var Qi=Object.getOwnPropertyNames,ht=Object.getOwnPropertySymbols,Ki=Object.getPrototypeOf,tr=Object.prototype.hasOwnProperty,Wr=Object.prototype.propertyIsEnumerable;var Ur=(e,t,r)=>t in e?dt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,$=(e,t)=>{for(var r in t||(t={}))tr.call(t,r)&&Ur(e,r,t[r]);if(ht)for(var r of ht(t))Wr.call(t,r)&&Ur(e,r,t[r]);return e};var Bi=e=>dt(e,"__esModule",{value:!0});var Dr=(e,t)=>{var r={};for(var o in e)tr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&ht)for(var o of ht(e))t.indexOf(o)<0&&Wr.call(e,o)&&(r[o]=e[o]);return r};var bt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Ji=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Qi(t))!tr.call(e,o)&&o!=="default"&&dt(e,o,{get:()=>t[o],enumerable:!(r=qi(t,o))||r.enumerable});return e},tt=e=>Ji(Bi(dt(e!=null?zi(Ki(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var zr=bt((rr,Nr)=>{(function(e,t){typeof rr=="object"&&typeof Nr!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(rr,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(w){return!!(w&&w!==document&&w.nodeName!=="HTML"&&w.nodeName!=="BODY"&&"classList"in w&&"contains"in w.classList)}function c(w){var Fe=w.type,Se=w.tagName;return!!(Se==="INPUT"&&a[Fe]&&!w.readOnly||Se==="TEXTAREA"&&!w.readOnly||w.isContentEditable)}function l(w){w.classList.contains("focus-visible")||(w.classList.add("focus-visible"),w.setAttribute("data-focus-visible-added",""))}function u(w){!w.hasAttribute("data-focus-visible-added")||(w.classList.remove("focus-visible"),w.removeAttribute("data-focus-visible-added"))}function m(w){w.metaKey||w.altKey||w.ctrlKey||(s(r.activeElement)&&l(r.activeElement),o=!0)}function p(w){o=!1}function b(w){!s(w.target)||(o||c(w.target))&&l(w.target)}function g(w){!s(w.target)||(w.target.classList.contains("focus-visible")||w.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),u(w.target))}function h(w){document.visibilityState==="hidden"&&(n&&(o=!0),W())}function W(){document.addEventListener("mousemove",j),document.addEventListener("mousedown",j),document.addEventListener("mouseup",j),document.addEventListener("pointermove",j),document.addEventListener("pointerdown",j),document.addEventListener("pointerup",j),document.addEventListener("touchmove",j),document.addEventListener("touchstart",j),document.addEventListener("touchend",j)}function q(){document.removeEventListener("mousemove",j),document.removeEventListener("mousedown",j),document.removeEventListener("mouseup",j),document.removeEventListener("pointermove",j),document.removeEventListener("pointerdown",j),document.removeEventListener("pointerup",j),document.removeEventListener("touchmove",j),document.removeEventListener("touchstart",j),document.removeEventListener("touchend",j)}function j(w){w.target.nodeName&&w.target.nodeName.toLowerCase()==="html"||(o=!1,q())}document.addEventListener("keydown",m,!0),document.addEventListener("mousedown",p,!0),document.addEventListener("pointerdown",p,!0),document.addEventListener("touchstart",p,!0),document.addEventListener("visibilitychange",h,!0),W(),r.addEventListener("focus",b,!0),r.addEventListener("blur",g,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var po=bt((Xa,xt)=>{var qr,Qr,Kr,Br,Jr,Yr,Gr,Xr,Zr,vt,or,eo,to,ro,De,oo,no,io,ao,so,co,lo,uo,gt;(function(e){var t=typeof global=="object"?global:typeof self=="object"?self:typeof this=="object"?this:{};typeof define=="function"&&define.amd?define("tslib",["exports"],function(o){e(r(t,r(o)))}):typeof xt=="object"&&typeof xt.exports=="object"?e(r(t,r(xt.exports))):e(r(t));function r(o,n){return o!==t&&(typeof Object.create=="function"?Object.defineProperty(o,"__esModule",{value:!0}):o.__esModule=!0),function(i,a){return o[i]=n?n(i,a):a}}})(function(e){var t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(o,n){o.__proto__=n}||function(o,n){for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(o[i]=n[i])};qr=function(o,n){if(typeof n!="function"&&n!==null)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");t(o,n);function i(){this.constructor=o}o.prototype=n===null?Object.create(n):(i.prototype=n.prototype,new i)},Qr=Object.assign||function(o){for(var n,i=1,a=arguments.length;i=0;u--)(l=o[u])&&(c=(s<3?l(c):s>3?l(n,i,c):l(n,i))||c);return s>3&&c&&Object.defineProperty(n,i,c),c},Jr=function(o,n){return function(i,a){n(i,a,o)}},Yr=function(o,n){if(typeof Reflect=="object"&&typeof Reflect.metadata=="function")return Reflect.metadata(o,n)},Gr=function(o,n,i,a){function s(c){return c instanceof i?c:new i(function(l){l(c)})}return new(i||(i=Promise))(function(c,l){function u(b){try{p(a.next(b))}catch(g){l(g)}}function m(b){try{p(a.throw(b))}catch(g){l(g)}}function p(b){b.done?c(b.value):s(b.value).then(u,m)}p((a=a.apply(o,n||[])).next())})},Xr=function(o,n){var i={label:0,sent:function(){if(c[0]&1)throw c[1];return c[1]},trys:[],ops:[]},a,s,c,l;return l={next:u(0),throw:u(1),return:u(2)},typeof Symbol=="function"&&(l[Symbol.iterator]=function(){return this}),l;function u(p){return function(b){return m([p,b])}}function m(p){if(a)throw new TypeError("Generator is already executing.");for(;i;)try{if(a=1,s&&(c=p[0]&2?s.return:p[0]?s.throw||((c=s.return)&&c.call(s),0):s.next)&&!(c=c.call(s,p[1])).done)return c;switch(s=0,c&&(p=[p[0]&2,c.value]),p[0]){case 0:case 1:c=p;break;case 4:return i.label++,{value:p[1],done:!1};case 5:i.label++,s=p[1],p=[0];continue;case 7:p=i.ops.pop(),i.trys.pop();continue;default:if(c=i.trys,!(c=c.length>0&&c[c.length-1])&&(p[0]===6||p[0]===2)){i=0;continue}if(p[0]===3&&(!c||p[1]>c[0]&&p[1]=o.length&&(o=void 0),{value:o&&o[a++],done:!o}}};throw new TypeError(n?"Object is not iterable.":"Symbol.iterator is not defined.")},or=function(o,n){var i=typeof Symbol=="function"&&o[Symbol.iterator];if(!i)return o;var a=i.call(o),s,c=[],l;try{for(;(n===void 0||n-- >0)&&!(s=a.next()).done;)c.push(s.value)}catch(u){l={error:u}}finally{try{s&&!s.done&&(i=a.return)&&i.call(a)}finally{if(l)throw l.error}}return c},eo=function(){for(var o=[],n=0;n1||u(h,W)})})}function u(h,W){try{m(a[h](W))}catch(q){g(c[0][3],q)}}function m(h){h.value instanceof De?Promise.resolve(h.value.v).then(p,b):g(c[0][2],h)}function p(h){u("next",h)}function b(h){u("throw",h)}function g(h,W){h(W),c.shift(),c.length&&u(c[0][0],c[0][1])}},no=function(o){var n,i;return n={},a("next"),a("throw",function(s){throw s}),a("return"),n[Symbol.iterator]=function(){return this},n;function a(s,c){n[s]=o[s]?function(l){return(i=!i)?{value:De(o[s](l)),done:s==="return"}:c?c(l):l}:c}},io=function(o){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var n=o[Symbol.asyncIterator],i;return n?n.call(o):(o=typeof vt=="function"?vt(o):o[Symbol.iterator](),i={},a("next"),a("throw"),a("return"),i[Symbol.asyncIterator]=function(){return this},i);function a(c){i[c]=o[c]&&function(l){return new Promise(function(u,m){l=o[c](l),s(u,m,l.done,l.value)})}}function s(c,l,u,m){Promise.resolve(m).then(function(p){c({value:p,done:u})},l)}},ao=function(o,n){return Object.defineProperty?Object.defineProperty(o,"raw",{value:n}):o.raw=n,o};var r=Object.create?function(o,n){Object.defineProperty(o,"default",{enumerable:!0,value:n})}:function(o,n){o.default=n};so=function(o){if(o&&o.__esModule)return o;var n={};if(o!=null)for(var i in o)i!=="default"&&Object.prototype.hasOwnProperty.call(o,i)&>(n,o,i);return r(n,o),n},co=function(o){return o&&o.__esModule?o:{default:o}},lo=function(o,n){if(!n.has(o))throw new TypeError("attempted to get private field on non-instance");return n.get(o)},uo=function(o,n,i){if(!n.has(o))throw new TypeError("attempted to set private field on non-instance");return n.set(o,i),i},e("__extends",qr),e("__assign",Qr),e("__rest",Kr),e("__decorate",Br),e("__param",Jr),e("__metadata",Yr),e("__awaiter",Gr),e("__generator",Xr),e("__exportStar",Zr),e("__createBinding",gt),e("__values",vt),e("__read",or),e("__spread",eo),e("__spreadArrays",to),e("__spreadArray",ro),e("__await",De),e("__asyncGenerator",oo),e("__asyncDelegator",no),e("__asyncValues",io),e("__makeTemplateObject",ao),e("__importStar",so),e("__importDefault",co),e("__classPrivateFieldGet",lo),e("__classPrivateFieldSet",uo)})});var Lr=bt((ft,Ar)=>{(function(t,r){typeof ft=="object"&&typeof Ar=="object"?Ar.exports=r():typeof define=="function"&&define.amd?define([],r):typeof ft=="object"?ft.ClipboardJS=r():t.ClipboardJS=r()})(ft,function(){return function(){var e={134:function(o,n,i){"use strict";i.d(n,{default:function(){return Di}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),m=i.n(u);function p(O){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?p=function(d){return typeof d}:p=function(d){return d&&typeof Symbol=="function"&&d.constructor===Symbol&&d!==Symbol.prototype?"symbol":typeof d},p(O)}function b(O,v){if(!(O instanceof v))throw new TypeError("Cannot call a class as a function")}function g(O,v){for(var d=0;d0&&arguments[0]!==void 0?arguments[0]:{};this.action=d.action,this.container=d.container,this.emitter=d.emitter,this.target=d.target,this.text=d.text,this.trigger=d.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"createFakeElement",value:function(){var d=document.documentElement.getAttribute("dir")==="rtl";this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[d?"right":"left"]="-9999px";var A=window.pageYOffset||document.documentElement.scrollTop;return this.fakeElem.style.top="".concat(A,"px"),this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.fakeElem}},{key:"selectFake",value:function(){var d=this,A=this.createFakeElement();this.fakeHandlerCallback=function(){return d.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.container.appendChild(A),this.selectedText=m()(A),this.copyText(),this.removeFake()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=m()(this.target),this.copyText()}},{key:"copyText",value:function(){var d;try{d=document.execCommand(this.action)}catch(A){d=!1}this.handleResult(d)}},{key:"handleResult",value:function(d){this.emitter.emit(d?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),document.activeElement.blur(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var d=arguments.length>0&&arguments[0]!==void 0?arguments[0]:"copy";if(this._action=d,this._action!=="copy"&&this._action!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(d){if(d!==void 0)if(d&&p(d)==="object"&&d.nodeType===1){if(this.action==="copy"&&d.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(this.action==="cut"&&(d.hasAttribute("readonly")||d.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`);this._target=d}else throw new Error('Invalid "target" value, use a valid Element')},get:function(){return this._target}}]),O}(),q=W;function j(O){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?j=function(d){return typeof d}:j=function(d){return d&&typeof Symbol=="function"&&d.constructor===Symbol&&d!==Symbol.prototype?"symbol":typeof d},j(O)}function w(O,v){if(!(O instanceof v))throw new TypeError("Cannot call a class as a function")}function Fe(O,v){for(var d=0;d0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof R.action=="function"?R.action:this.defaultAction,this.target=typeof R.target=="function"?R.target:this.defaultTarget,this.text=typeof R.text=="function"?R.text:this.defaultText,this.container=j(R.container)==="object"?R.container:document.body}},{key:"listenClick",value:function(R){var X=this;this.listener=l()(R,"click",function(et){return X.onClick(et)})}},{key:"onClick",value:function(R){var X=R.delegateTarget||R.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new q({action:this.action(X),target:this.target(X),text:this.text(X),container:this.container,trigger:X,emitter:this})}},{key:"defaultAction",value:function(R){return er("action",R)}},{key:"defaultTarget",value:function(R){var X=er("target",R);if(X)return document.querySelector(X)}},{key:"defaultText",value:function(R){return er("text",R)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var R=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],X=typeof R=="string"?[R]:R,et=!!document.queryCommandSupported;return X.forEach(function(Ni){et=et&&!!document.queryCommandSupported(Ni)}),et}}]),d}(s()),Di=Ui},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(u,m,p,b,g){var h=l.apply(this,arguments);return u.addEventListener(p,h,g),{destroy:function(){u.removeEventListener(p,h,g)}}}function c(u,m,p,b,g){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof p=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(h){return s(h,m,p,b,g)}))}function l(u,m,p,b){return function(g){g.delegateTarget=a(g.target,m),g.delegateTarget&&b.call(u,g)}}o.exports=c},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function c(p,b,g){if(!p&&!b&&!g)throw new Error("Missing required arguments");if(!a.string(b))throw new TypeError("Second argument must be a String");if(!a.fn(g))throw new TypeError("Third argument must be a Function");if(a.node(p))return l(p,b,g);if(a.nodeList(p))return u(p,b,g);if(a.string(p))return m(p,b,g);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(p,b,g){return p.addEventListener(b,g),{destroy:function(){p.removeEventListener(b,g)}}}function u(p,b,g){return Array.prototype.forEach.call(p,function(h){h.addEventListener(b,g)}),{destroy:function(){Array.prototype.forEach.call(p,function(h){h.removeEventListener(b,g)})}}}function m(p,b,g){return s(document.body,p,b,g)}o.exports=c},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c{"use strict";var Ia=/["'&<>]/;mi.exports=Pa;function Pa(e){var t=""+e,r=Ia.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=o.hasError,i=o.isStopped,a=o.observers;return n||i?nr:(a.push(r),new ie(function(){return we(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new _;return r.source=this,r},t.create=function(r,o){return new To(r,o)},t}(_);var To=function(e){K(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:nr},t}(M);var nt={now:function(){return(nt.delegate||Date).now()},delegate:void 0};var it=function(e){K(t,e);function t(r,o,n){r===void 0&&(r=Infinity),o===void 0&&(o=Infinity),n===void 0&&(n=nt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===Infinity,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,c=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),c=0;c0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=ze.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){if(n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);r.actions.length===0&&(ze.cancelAnimationFrame(o),r._scheduled=void 0)},t}(_t);var Lo=function(e){K(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0,this._scheduled=void 0;var o=this.actions,n,i=-1;r=r||o.shift();var a=o.length;do if(n=r.execute(r.state,r.delay))break;while(++i=2,!0))}function ne(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new M}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u=null,m=null,p=null,b=0,g=!1,h=!1,W=function(){m==null||m.unsubscribe(),m=null},q=function(){W(),u=p=null,g=h=!1},j=function(){var w=u;q(),w==null||w.unsubscribe()};return x(function(w,Fe){b++,!h&&!g&&W();var Se=p=p!=null?p:r();Fe.add(function(){b--,b===0&&!h&&!g&&(m=xr(j,c))}),Se.subscribe(Fe),u||(u=new ot({next:function(Ue){return Se.next(Ue)},error:function(Ue){h=!0,W(),m=xr(q,n,Ue),Se.error(Ue)},complete:function(){g=!0,W(),m=xr(q,a),Se.complete()}}),ve(w).subscribe(u))})(l)}}function xr(e,t){for(var r=[],o=2;ot==="focus"),V(e===Ie()))}var Go=new M,wa=Oe(()=>F(new ResizeObserver(e=>{for(let t of e)Go.next(t)}))).pipe(E(e=>Z.pipe(V(e)).pipe(P(()=>e.disconnect()))),re(1));function Ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Dt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function He(e){return wa.pipe(H(t=>t.observe(e)),E(t=>Go.pipe(k(({target:r})=>r===e),P(()=>t.unobserve(e)),f(()=>Ce(e)))),V(Ce(e)))}function Xo(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ea(e){return I(T(e,"scroll"),T(window,"resize")).pipe(f(()=>Xo(e)),V(Xo(e)))}function Zo(e,t=16){return Ea(e).pipe(f(({y:r})=>{let o=Ce(e),n=Dt(e);return r>=n.height-o.height-t}),z())}function en(e){if(e instanceof HTMLInputElement)e.select();else throw new Error("Not implemented")}var Nt={drawer:fe("[data-md-toggle=drawer]"),search:fe("[data-md-toggle=search]")};function tn(e){return Nt[e].checked}function $e(e,t){Nt[e].checked!==t&&Nt[e].click()}function zt(e){let t=Nt[e];return T(t,"change").pipe(f(()=>t.checked),V(t.checked))}function Oa(e){switch(e.tagName){case"INPUT":case"SELECT":case"TEXTAREA":return!0;default:return e.isContentEditable}}function rn(){return T(window,"keydown").pipe(k(e=>!(e.metaKey||e.ctrlKey)),f(e=>({mode:tn("search")?"search":"global",type:e.key,claim(){e.preventDefault(),e.stopPropagation()}})),k(({mode:e})=>{if(e==="global"){let t=Ie();if(typeof t!="undefined")return!Oa(t)}return!0}),ne())}function on(){return new URL(location.href)}function nn(e){location.href=e.href}function an(){return new M}function sn(){return location.hash.substring(1)}function cn(e){let t=Ge("a");t.href=e,t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Ta(){return T(window,"hashchange").pipe(f(sn),V(sn()),k(e=>e.length>0),ne())}function ln(){return Ta().pipe(E(e=>F(ae(`[id="${e}"]`))))}function pt(e){let t=matchMedia(e);return Vt(r=>t.addListener(()=>r(t.matches))).pipe(V(t.matches))}function un(){return T(window,"beforeprint").pipe(oe(void 0))}function _r(e,t){return e.pipe(E(r=>r?t():Z))}function qt(e,t={credentials:"same-origin"}){return ve(fetch(`${e}`,t)).pipe(k(r=>r.status===200))}function ye(e,t){return qt(e,t).pipe(E(r=>r.json()),re(1))}function pn(e,t){let r=new DOMParser;return qt(e,t).pipe(E(o=>o.text()),f(o=>r.parseFromString(o,"text/xml")),re(1))}function fn(){return{x:Math.max(0,pageXOffset),y:Math.max(0,pageYOffset)}}function Mr({x:e,y:t}){window.scrollTo(e||0,t||0)}function mn(){return I(T(window,"scroll",{passive:!0}),T(window,"resize",{passive:!0})).pipe(f(fn),V(fn()))}function dn(){return{width:innerWidth,height:innerHeight}}function hn(){return T(window,"resize",{passive:!0}).pipe(f(dn),V(dn()))}function bn(){return Y([mn(),hn()]).pipe(f(([e,t])=>({offset:e,size:t})),re(1))}function Qt(e,{viewport$:t,header$:r}){let o=t.pipe(U("size")),n=Y([o,r]).pipe(f(()=>({x:e.offsetLeft,y:e.offsetTop})));return Y([r,t,n]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}function vn(e,{tx$:t}){let r=T(e,"message").pipe(f(({data:o})=>o));return t.pipe(Or(()=>r,{leading:!0,trailing:!0}),H(o=>e.postMessage(o)),Sr(r),ne())}var _a=fe("#__config"),Xe=JSON.parse(_a.textContent);Xe.base=new URL(Xe.base,on()).toString().replace(/\/$/,"");function se(){return Xe}function Kt(e){return Xe.features.includes(e)}function G(e,t){return typeof t!="undefined"?Xe.translations[e].replace("#",t.toString()):Xe.translations[e]}function je(e,t=document){return fe(`[data-md-component=${e}]`,t)}function me(e,t=document){return Q(`[data-md-component=${e}]`,t)}var Zn=tt(Lr());function gn(e,t=0){e.setAttribute("tabindex",t.toString())}function xn(e){e.removeAttribute("tabindex")}function yn(e,t){e.setAttribute("data-md-state","lock"),e.style.top=`-${t}px`}function Sn(e){let t=-1*parseInt(e.style.top,10);e.removeAttribute("data-md-state"),e.style.top="",t&&window.scrollTo(0,t)}function wn(e,t){e.setAttribute("data-md-state",t)}function En(e){e.removeAttribute("data-md-state")}function On(e,t){e.classList.toggle("md-nav__link--active",t)}function Tn(e){e.classList.remove("md-nav__link--active")}function _n(e,t){e.firstElementChild.innerHTML=t}function Mn(e,t){e.setAttribute("data-md-state",t)}function An(e){e.removeAttribute("data-md-state")}function Ln(e,t){e.setAttribute("data-md-state",t)}function kn(e){e.removeAttribute("data-md-state")}function Cn(e,t){e.setAttribute("data-md-state",t)}function Hn(e){e.removeAttribute("data-md-state")}function jn(e,t){e.placeholder=t}function Fn(e){e.placeholder=G("search.placeholder")}function Rn(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Rn(e,r)}function D(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="boolean"?o.setAttribute(n,t[n]):t[n]&&o.setAttribute(n,"");for(let n of r)Rn(o,n);return o}function In(e,t){let r=t;if(e.length>r){for(;e[r]!==" "&&--r>0;);return`${e.substring(0,r)}...`}return e}function Bt(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function Pn(e,t){switch(t){case 0:e.textContent=G("search.result.none");break;case 1:e.textContent=G("search.result.one");break;default:e.textContent=G("search.result.other",Bt(t))}}function kr(e){e.textContent=G("search.result.placeholder")}function $n(e,t){e.appendChild(t)}function Vn(e){e.innerHTML=""}function Wn(e,t){e.style.top=`${t}px`}function Un(e){e.style.top=""}function Dn(e,t){let r=e.firstElementChild;r.style.height=`${t-2*r.offsetTop}px`}function Nn(e){let t=e.firstElementChild;t.style.height=""}function zn(e,t){e.lastElementChild.appendChild(t)}function qn(e,t){e.lastElementChild.setAttribute("data-md-state",t)}function Qn(e,t){e.setAttribute("data-md-state",t)}function Cr(e){e.removeAttribute("data-md-state")}function Kn(e,t){e.setAttribute("data-md-state",t)}function Hr(e){e.removeAttribute("data-md-state")}function Bn(e){return D("button",{class:"md-clipboard md-icon",title:G("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}var Ve;(function(r){r[r.TEASER=1]="TEASER",r[r.PARENT=2]="PARENT"})(Ve||(Ve={}));function jr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(a=>!e.terms[a]).map(a=>[D("del",null,a)," "]).flat().slice(0,-1),i=e.location;return D("a",{href:i,class:"md-search-result__link",tabIndex:-1},D("article",{class:["md-search-result__article",...r?["md-search-result__article--document"]:[]].join(" "),"data-md-score":e.score.toFixed(2)},r>0&&D("div",{class:"md-search-result__icon md-icon"}),D("h1",{class:"md-search-result__title"},e.title),o>0&&e.text.length>0&&D("p",{class:"md-search-result__teaser"},In(e.text,320)),o>0&&n.length>0&&D("p",{class:"md-search-result__terms"},G("search.result.term.missing"),": ",n)))}function Jn(e){let t=e[0].score,r=[...e],o=r.findIndex(l=>!l.location.includes("#")),[n]=r.splice(o,1),i=r.findIndex(l=>l.scorejr(l,1)),...s.length?[D("details",{class:"md-search-result__more"},D("summary",{tabIndex:-1},s.length>0&&s.length===1?G("search.result.more.one"):G("search.result.more.other",s.length)),s.map(l=>jr(l,1)))]:[]];return D("li",{class:"md-search-result__item"},c)}function Yn(e){return D("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>D("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Bt(r):r)))}function Gn(e){return D("div",{class:"md-typeset__scrollwrap"},D("div",{class:"md-typeset__table"},e))}function Ma(e){let t=se(),r=new URL(`${e.version}/`,t.base);return D("li",{class:"md-version__item"},D("a",{href:r.toString(),class:"md-version__link"},e.title))}function Xn(e){let t=se(),[,r]=t.base.match(/([^/]+)\/?$/),o=e.find(({version:n,aliases:i})=>n===r||i.includes(r))||e[0];return D("div",{class:"md-version"},D("button",{class:"md-version__current"},o.title),D("ul",{class:"md-version__list"},e.map(Ma)))}var Aa=0;function La(e,{viewport$:t}){let r=F(e).pipe(E(o=>{let n=o.closest("[data-tabs]");return n instanceof HTMLElement?I(...Q("input",n).map(i=>T(i,"change"))):Z}));return I(t.pipe(U("size")),r).pipe(f(()=>{let o=Ce(e);return{scroll:Dt(e).width>o.width}}),U("scroll"))}function ei(e,t){let r=new M;if(r.pipe(be(pt("(hover)"))).subscribe(([{scroll:o},n])=>{o&&n?gn(e):xn(e)}),Zn.default.isSupported()){let o=e.closest("pre");o.id=`__code_${Aa++}`,o.insertBefore(Bn(o.id),e)}return La(e,t).pipe(H(r),P(()=>r.complete()),f(o=>$({ref:e},o)))}function ka(e,{target$:t,print$:r}){return t.pipe(f(o=>o.closest("details:not([open])")),k(o=>e===o),ut(r),oe(e))}function ti(e,t){let r=new M;return r.subscribe(()=>{e.setAttribute("open",""),e.scrollIntoView()}),ka(e,t).pipe(H(r),P(()=>r.complete()),oe({ref:e}))}var ri=Ge("table");function oi(e){return Pe(e,ri),Pe(ri,Gn(e)),F({ref:e})}function ni(e,{target$:t,viewport$:r,print$:o}){return I(...Q("pre > code",e).map(n=>ei(n,{viewport$:r})),...Q("table:not([class])",e).map(n=>oi(n)),...Q("details",e).map(n=>ti(n,{target$:t,print$:o})))}function Ca(e,{alert$:t}){return t.pipe(E(r=>I(F(!0),F(!1).pipe(Te(2e3))).pipe(f(o=>({message:r,open:o})))))}function ii(e,t){let r=new M;return r.pipe(J(B)).subscribe(({message:o,open:n})=>{_n(e,o),n?Mn(e,"open"):An(e)}),Ca(e,t).pipe(H(r),P(()=>r.complete()),f(o=>$({ref:e},o)))}function Ha({viewport$:e}){if(!Kt("header.autohide"))return F(!1);let t=e.pipe(f(({offset:{y:n}})=>n),he(2,1),f(([n,i])=>[nMath.abs(i-n.y)>100),f(([,[n]])=>n),z()),o=zt("search");return Y([e,o]).pipe(f(([{offset:n},i])=>n.y>400&&!i),z(),E(n=>n?r:F(!1)),V(!1))}function ai(e,t){return Oe(()=>{let r=getComputedStyle(e);return F(r.position==="sticky"||r.position==="-webkit-sticky")}).pipe(Wt(He(e),Ha(t)),f(([r,{height:o},n])=>({height:r?o:0,sticky:r,hidden:n})),z((r,o)=>r.sticky===o.sticky&&r.height===o.height&&r.hidden===o.hidden),re(1))}function si(e,{header$:t,main$:r}){let o=new M;return o.pipe(U("active"),Wt(t),J(B)).subscribe(([{active:n},{hidden:i}])=>{n?Ln(e,i?"hidden":"shadow"):kn(e)}),r.subscribe(n=>o.next(n)),t.pipe(f(n=>$({ref:e},n)))}function ja(e,{viewport$:t,header$:r}){return Qt(e,{header$:r,viewport$:t}).pipe(f(({offset:{y:o}})=>{let{height:n}=Ce(e);return{active:o>=n}}),U("active"))}function ci(e,t){let r=new M;r.pipe(J(B)).subscribe(({active:n})=>{n?Cn(e,"active"):Hn(e)});let o=ae("article h1");return typeof o=="undefined"?Z:ja(o,t).pipe(H(r),P(()=>r.complete()),f(n=>$({ref:e},n)))}function li(e,{viewport$:t,header$:r}){let o=r.pipe(f(({height:i})=>i),z()),n=o.pipe(E(()=>He(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),U("bottom"))));return Y([o,n,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),z((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function Fa(e){let t=localStorage.getItem(__prefix("__palette")),r=JSON.parse(t)||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},o=F(...e).pipe(te(n=>T(n,"change").pipe(oe(n))),V(e[Math.max(0,r.index)]),f(n=>({index:e.indexOf(n),color:{scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),re(1));return o.subscribe(n=>{localStorage.setItem(__prefix("__palette"),JSON.stringify(n))}),o}function ui(e){let t=new M;t.subscribe(o=>{for(let[n,i]of Object.entries(o.color))typeof i=="string"&&document.body.setAttribute(`data-md-color-${n}`,i);for(let n=0;nt.complete()),f(o=>$({ref:e},o)))}var Fr=tt(Lr());function pi({alert$:e}){Fr.default.isSupported()&&new _(t=>{new Fr.default("[data-clipboard-target], [data-clipboard-text]").on("success",r=>t.next(r))}).subscribe(()=>e.next(G("clipboard.copied")))}function Ra(e){if(e.length<2)return e;let[t,r]=e.sort((i,a)=>i.length-a.length).map(i=>i.replace(/[^/]+$/,"")),o=0;if(t===r)o=t.length;else for(;t.charCodeAt(o)===r.charCodeAt(o);)o++;let n=se();return e.map(i=>i.replace(t.slice(0,o),`${n.base}/`))}function fi({document$:e,location$:t,viewport$:r}){let o=se();if(location.protocol==="file:")return;"scrollRestoration"in history&&(history.scrollRestoration="manual",T(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}));let n=ae("link[rel=icon]");typeof n!="undefined"&&(n.href=n.href);let i=pn(`${o.base}/sitemap.xml`).pipe(f(l=>Ra(Q("loc",l).map(u=>u.textContent))),E(l=>T(document.body,"click").pipe(k(u=>!u.metaKey&&!u.ctrlKey),E(u=>{if(u.target instanceof Element){let m=u.target.closest("a");if(m&&!m.target&&l.includes(m.href))return u.preventDefault(),F({url:new URL(m.href)})}return Z}))),ne()),a=T(window,"popstate").pipe(k(l=>l.state!==null),f(l=>({url:new URL(location.href),offset:l.state})),ne());I(i,a).pipe(z((l,u)=>l.url.href===u.url.href),f(({url:l})=>l)).subscribe(t);let s=t.pipe(U("pathname"),E(l=>qt(l.href).pipe(Je(()=>(nn(l),Z)))),ne());i.pipe(Ye(s)).subscribe(({url:l})=>{history.pushState({},"",`${l}`)});let c=new DOMParser;s.pipe(E(l=>l.text()),f(l=>c.parseFromString(l,"text/html"))).subscribe(e),I(i,a).pipe(Ye(e)).subscribe(({url:l,offset:u})=>{l.hash&&!u?cn(l.hash):Mr(u||{y:0})}),e.pipe(Ut(1)).subscribe(l=>{for(let u of["title","link[rel=canonical]","meta[name=author]","meta[name=description]","[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=logo], .md-logo","[data-md-component=skip]"]){let m=ae(u),p=ae(u,l);typeof m!="undefined"&&typeof p!="undefined"&&Pe(m,p)}}),e.pipe(Ut(1),f(()=>je("container")),E(l=>F(...Q("script",l))),dr(l=>{let u=Ge("script");if(l.src){for(let m of l.getAttributeNames())u.setAttribute(m,l.getAttribute(m));return Pe(l,u),new _(m=>{u.onload=()=>m.complete()})}else return u.textContent=l.textContent,Pe(l,u),de})).subscribe(),r.pipe(yr(i),hr(250),U("offset")).subscribe(({offset:l})=>{history.replaceState(l,"")}),I(i,a).pipe(he(2,1),k(([l,u])=>l.url.pathname===u.url.pathname),f(([,l])=>l)).subscribe(({offset:l})=>{Mr(l||{y:0})})}var $a=tt(di());function hi(e){return e.split(/"([^"]+)"/g).map((t,r)=>r&1?t.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g," +"):t).join("").replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g,"").trim()}var _e;(function(n){n[n.SETUP=0]="SETUP",n[n.READY=1]="READY",n[n.QUERY=2]="QUERY",n[n.RESULT=3]="RESULT"})(_e||(_e={}));function Jt(e){return e.type===1}function bi(e){return e.type===2}function Yt(e){return e.type===3}function Va({config:e,docs:t,index:r}){e.lang.length===1&&e.lang[0]==="en"&&(e.lang=[G("search.config.lang")]),e.separator==="[\\s\\-]+"&&(e.separator=G("search.config.separator"));let o=G("search.config.pipeline").split(/\s*,\s*/).filter(Boolean);return{config:e,docs:t,index:r,pipeline:o}}function vi(e,t){let r=se(),o=new Worker(e),n=new M,i=vn(o,{tx$:n}).pipe(f(a=>{if(Yt(a))for(let s of a.data)for(let c of s)c.location=`${r.base}/${c.location}`;return a}),ne());return ve(t).pipe(f(a=>({type:_e.SETUP,data:Va(a)}))).subscribe(n.next.bind(n)),{tx$:n,rx$:i}}function gi(){let e=se();ye(new URL("versions.json",e.base)).subscribe(t=>{fe(".md-header__topic").appendChild(Xn(t))})}function Wa(e){let t=(__search==null?void 0:__search.transform)||hi,r=Yo(e),o=I(T(e,"keyup"),T(e,"focus").pipe(Te(1))).pipe(f(()=>t(e.value)),z());return Y([o,r]).pipe(f(([n,i])=>({value:n,focus:i})))}function xi(e,{tx$:t}){let r=new M;return r.pipe(U("value"),f(({value:o})=>({type:_e.QUERY,data:o}))).subscribe(t.next.bind(t)),r.pipe(U("focus")).subscribe(({focus:o})=>{o?($e("search",o),jn(e,"")):Fn(e)}),T(e.form,"reset").pipe(wr(r.pipe(vr(1)))).subscribe(()=>ke(e)),Wa(e).pipe(H(r),P(()=>r.complete()),f(o=>$({ref:e},o)))}function yi(e,{rx$:t},{query$:r}){let o=new M,n=Zo(e.parentElement).pipe(k(Boolean)),i=fe(":scope > :first-child",e),a=fe(":scope > :last-child",e);return t.pipe(k(Jt),xe(1)).subscribe(()=>{kr(i)}),o.pipe(J(B),be(r)).subscribe(([{data:c},{value:l}])=>{l?Pn(i,c.length):kr(i)}),o.pipe(J(B),H(()=>Vn(a)),E(({data:c})=>I(F(...c.slice(0,10)),F(...c.slice(10)).pipe(he(4),Tr(n),E(([l])=>F(...l)))))).subscribe(c=>{$n(a,Jn(c))}),t.pipe(k(Yt),f(({data:c})=>({data:c})),V({data:[]})).pipe(H(o),P(()=>o.complete()),f(c=>$({ref:e},c)))}function Si(e,{index$:t,keyboard$:r}){let o=se(),n=vi(o.search,t),i=je("search-query",e),a=je("search-result",e),{tx$:s,rx$:c}=n;s.pipe(k(bi),Ye(c.pipe(k(Jt))),xe(1)).subscribe(s.next.bind(s)),r.pipe(k(({mode:u})=>u==="search")).subscribe(u=>{let m=Ie();switch(u.type){case"Enter":m===i&&u.claim();break;case"Escape":case"Tab":$e("search",!1),ke(i,!1);break;case"ArrowUp":case"ArrowDown":if(typeof m=="undefined")ke(i);else{let p=[i,...Q(":not(details) > [href], summary, details[open] [href]",a)],b=Math.max(0,(Math.max(0,p.indexOf(m))+p.length+(u.type==="ArrowUp"?-1:1))%p.length);ke(p[b])}u.claim();break;default:i!==Ie()&&ke(i)}}),r.pipe(k(({mode:u})=>u==="global")).subscribe(u=>{switch(u.type){case"f":case"s":case"/":ke(i),en(i),u.claim();break}});let l=xi(i,n);return I(l,yi(a,n,{query$:l}))}function Ua(e,{viewport$:t,main$:r}){let o=e.parentElement.offsetTop-e.parentElement.parentElement.offsetTop;return Y([r,t]).pipe(f(([{offset:n,height:i},{offset:{y:a}}])=>(i=i+Math.min(o,Math.max(0,a-n))-o,{height:i,locked:a>=n+o})),z((n,i)=>n.height===i.height&&n.locked===i.locked))}function Rr(e,o){var n=o,{header$:t}=n,r=Dr(n,["header$"]);let i=new M;return i.pipe(J(B),be(t)).subscribe({next([{height:a},{height:s}]){Dn(e,a),Wn(e,s)},complete(){Un(e),Nn(e)}}),Ua(e,r).pipe(H(i),P(()=>i.complete()),f(a=>$({ref:e},a)))}function wi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return lt(ye(`${r}/releases/latest`).pipe(f(o=>({version:o.tag_name})),Re({})),ye(r).pipe(f(o=>({stars:o.stargazers_count,forks:o.forks_count})),Re({}))).pipe(f(([o,n])=>$($({},o),n)))}else{let r=`https://api.github.com/repos/${e}`;return ye(r).pipe(f(o=>({repositories:o.public_repos})),Re({}))}}function Ei(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return ye(r).pipe(f(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Re({}))}function Oi(e){let[t]=e.match(/(git(?:hub|lab))/i)||[];switch(t.toLowerCase()){case"github":let[,r,o]=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);return wi(r,o);case"gitlab":let[,n,i]=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i);return Ei(n,i);default:return Z}}var Da;function Na(e){return Da||(Da=Oe(()=>{let t=sessionStorage.getItem(__prefix("__source"));if(t)return F(JSON.parse(t));{let r=Oi(e.href);return r.subscribe(o=>{try{sessionStorage.setItem(__prefix("__source"),JSON.stringify(o))}catch(n){}}),r}}).pipe(Je(()=>Z),k(t=>Object.keys(t).length>0),f(t=>({facts:t})),re(1)))}function Ti(e){let t=new M;return t.subscribe(({facts:r})=>{zn(e,Yn(r)),qn(e,"done")}),Na(e).pipe(H(t),P(()=>t.complete()),f(r=>$({ref:e},r)))}function za(e,{viewport$:t,header$:r}){return He(document.body).pipe(E(()=>Qt(e,{header$:r,viewport$:t})),f(({offset:{y:o}})=>({hidden:o>=10})),U("hidden"))}function _i(e,t){let r=new M;return r.pipe(J(B)).subscribe({next({hidden:o}){o?Qn(e,"hidden"):Cr(e)},complete(){Cr(e)}}),za(e,t).pipe(H(r),P(()=>r.complete()),f(o=>$({ref:e},o)))}function qa(e,{viewport$:t,header$:r}){let o=new Map;for(let a of e){let s=decodeURIComponent(a.hash.substring(1)),c=ae(`[id="${s}"]`);typeof c!="undefined"&&o.set(a,c)}let n=r.pipe(f(a=>24+a.height));return He(document.body).pipe(U("height"),f(()=>{let a=[];return[...o].reduce((s,[c,l])=>{for(;a.length&&o.get(a[a.length-1]).tagName>=l.tagName;)a.pop();let u=l.offsetTop;for(;!u&&l.parentElement;)l=l.parentElement,u=l.offsetTop;return s.set([...a=[...a,c]].reverse(),u)},new Map)}),f(a=>new Map([...a].sort(([,s],[,c])=>s-c))),E(a=>Y([n,t]).pipe(gr(([s,c],[l,{offset:{y:u}}])=>{for(;c.length;){let[,m]=c[0];if(m-l=u)c=[s.pop(),...c];else break}return[s,c]},[[],[...a]]),z((s,c)=>s[0]===c[0]&&s[1]===c[1])))).pipe(f(([a,s])=>({prev:a.map(([c])=>c),next:s.map(([c])=>c)})),V({prev:[],next:[]}),he(2,1),f(([a,s])=>a.prev.length{for(let[a]of i)Tn(a),En(a);for(let[a,[s]]of n.entries())On(s,a===n.length-1),wn(s,"blur")});let o=Q("[href^=\\#]",e);return qa(o,t).pipe(H(r),P(()=>r.complete()),f(n=>$({ref:e},n)))}function Qa(e,{viewport$:t,main$:r}){let o=t.pipe(f(({offset:{y:i}})=>i),he(2,1),f(([i,a])=>i>a),z()),n=r.pipe(U("active"));return Y([n,o]).pipe(f(([{active:i},a])=>({hidden:!(i&&a)})),z((i,a)=>i.hidden===a.hidden))}function Ai(e,t){let r=new M;return r.pipe(J(B)).subscribe({next({hidden:o}){o?Kn(e,"hidden"):Hr(e)},complete(){Hr(e)}}),Qa(e,t).pipe(H(r),P(()=>r.complete()),f(o=>$({ref:e},o)))}function Li({document$:e,tablet$:t}){e.pipe(E(()=>F(...Q("[data-md-state=indeterminate]"))),H(r=>{r.indeterminate=!0,r.checked=!1}),te(r=>T(r,"change").pipe(Er(()=>r.hasAttribute("data-md-state")),oe(r))),be(t)).subscribe(([r,o])=>{r.removeAttribute("data-md-state"),o&&(r.checked=!1)})}function Ka(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function ki({document$:e}){e.pipe(E(()=>F(...Q("[data-md-scrollfix]"))),H(t=>t.removeAttribute("data-md-scrollfix")),k(Ka),te(t=>T(t,"touchstart").pipe(oe(t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Ci({viewport$:e,tablet$:t}){Y([zt("search"),t]).pipe(f(([r,o])=>r&&!o),E(r=>F(r).pipe(Te(r?400:100),J(B))),be(e)).subscribe(([r,{offset:{y:o}}])=>{r?yn(document.body,o):Sn(document.body)})}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var Ze=Jo(),Ir=an(),Pr=ln(),$r=rn(),ue=bn(),Gt=pt("(min-width: 960px)"),Hi=pt("(min-width: 1220px)"),ji=un(),Fi=se(),Ba=document.forms.namedItem("search")?(__search==null?void 0:__search.index)||ye(`${Fi.base}/search/search_index.json`):Z,Vr=new M;pi({alert$:Vr});Kt("navigation.instant")&&fi({document$:Ze,location$:Ir,viewport$:ue});var Ii;((Ii=Fi.version)==null?void 0:Ii.provider)==="mike"&&gi();I(Ir,Pr).pipe(Te(125)).subscribe(()=>{$e("drawer",!1),$e("search",!1)});$r.pipe(k(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=ae("[href][rel=prev]");typeof t!="undefined"&&t.click();break;case"n":case".":let r=ae("[href][rel=next]");typeof r!="undefined"&&r.click();break}});Li({document$:Ze,tablet$:Gt});ki({document$:Ze});Ci({viewport$:ue,tablet$:Gt});var We=ai(je("header"),{viewport$:ue}),Xt=Ze.pipe(f(()=>je("main")),E(e=>li(e,{viewport$:ue,header$:We})),re(1)),Ja=I(...me("dialog").map(e=>ii(e,{alert$:Vr})),...me("header").map(e=>si(e,{viewport$:ue,header$:We,main$:Xt})),...me("palette").map(e=>ui(e)),...me("search").map(e=>Si(e,{index$:Ba,keyboard$:$r})),...me("source").map(e=>Ti(e))),Ya=Oe(()=>I(...me("content").map(e=>ni(e,{target$:Pr,viewport$:ue,print$:ji})),...me("header-title").map(e=>ci(e,{viewport$:ue,header$:We})),...me("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?_r(Hi,()=>Rr(e,{viewport$:ue,header$:We,main$:Xt})):_r(Gt,()=>Rr(e,{viewport$:ue,header$:We,main$:Xt}))),...me("tabs").map(e=>_i(e,{viewport$:ue,header$:We})),...me("toc").map(e=>Mi(e,{viewport$:ue,header$:We})),...me("top").map(e=>Ai(e,{viewport$:ue,main$:Xt})))),Ri=Ze.pipe(E(()=>Ya),ut(Ja),re(1));Ri.subscribe();window.document$=Ze;window.location$=Ir;window.target$=Pr;window.keyboard$=$r;window.viewport$=ue;window.tablet$=Gt;window.screen$=Hi;window.print$=ji;window.alert$=Vr;window.component$=Ri;})(); +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */ +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ +//# sourceMappingURL=bundle.34eae1b6.min.js.map + diff --git a/v10.0/assets/javascripts/workers/search.d351de03.min.js b/v10.0/assets/javascripts/workers/search.d351de03.min.js new file mode 100644 index 00000000..84aa5bd5 --- /dev/null +++ b/v10.0/assets/javascripts/workers/search.d351de03.min.js @@ -0,0 +1,61 @@ +(()=>{var le=Object.create,U=Object.defineProperty;var he=Object.getOwnPropertyDescriptor;var de=Object.getOwnPropertyNames;var fe=Object.getPrototypeOf,pe=Object.prototype.hasOwnProperty;var ge=t=>U(t,"__esModule",{value:!0});var q=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var ye=(t,e,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of de(e))!pe.call(t,n)&&n!=="default"&&U(t,n,{get:()=>e[n],enumerable:!(r=he(e,n))||r.enumerable});return t},Y=t=>ye(ge(U(t!=null?le(fe(t)):{},"default",t&&t.__esModule&&"default"in t?{get:()=>t.default,enumerable:!0}:{value:t,enumerable:!0})),t);var z=(t,e,r)=>new Promise((n,i)=>{var s=u=>{try{a(r.next(u))}catch(c){i(c)}},o=u=>{try{a(r.throw(u))}catch(c){i(c)}},a=u=>u.done?n(u.value):Promise.resolve(u.value).then(s,o);a((r=r.apply(t,e)).next())});var X=q((G,J)=>{(function(){var t=function(e){var r=new t.Builder;return r.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),r.searchPipeline.add(t.stemmer),e.call(r,r),r.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(r){e.console&&console.warn&&console.warn(r)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var r=Object.create(null),n=Object.keys(e),i=0;i0){var d=t.utils.clone(r)||{};d.position=[a,c],d.index=s.length,s.push(new t.Token(n.slice(a,o),d))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,r){r in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+r),e.label=r,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var r=e.label&&e.label in this.registeredFunctions;r||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. +`,e)},t.Pipeline.load=function(e){var r=new t.Pipeline;return e.forEach(function(n){var i=t.Pipeline.registeredFunctions[n];if(i)r.add(i);else throw new Error("Cannot load unregistered function: "+n)}),r},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(r){t.Pipeline.warnIfFunctionNotRegistered(r),this._stack.push(r)},this)},t.Pipeline.prototype.after=function(e,r){t.Pipeline.warnIfFunctionNotRegistered(r);var n=this._stack.indexOf(e);if(n==-1)throw new Error("Cannot find existingFn");n=n+1,this._stack.splice(n,0,r)},t.Pipeline.prototype.before=function(e,r){t.Pipeline.warnIfFunctionNotRegistered(r);var n=this._stack.indexOf(e);if(n==-1)throw new Error("Cannot find existingFn");this._stack.splice(n,0,r)},t.Pipeline.prototype.remove=function(e){var r=this._stack.indexOf(e);r!=-1&&this._stack.splice(r,1)},t.Pipeline.prototype.run=function(e){for(var r=this._stack.length,n=0;n1&&(oe&&(n=s),o!=e);)i=n-r,s=r+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ou?d+=2:a==u&&(r+=n[c+1]*i[d+1],c+=2,d+=2);return r},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),r=1,n=0;r0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}if(s.str.length==0&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var c=s.node.edges["*"];else{var c=new t.TokenSet;s.node.edges["*"]=c}s.str.length==1&&(c.final=!0),i.push({node:c,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var d=s.str.charAt(0),y=s.str.charAt(1),p;y in s.node.edges?p=s.node.edges[y]:(p=new t.TokenSet,s.node.edges[y]=p),s.str.length==1&&(p.final=!0),i.push({node:p,editsRemaining:s.editsRemaining-1,str:d+s.str.slice(2)})}}}return n},t.TokenSet.fromString=function(e){for(var r=new t.TokenSet,n=r,i=0,s=e.length;i=e;r--){var n=this.uncheckedNodes[r],i=n.child.toString();i in this.minimizedNodes?n.parent.edges[n.char]=this.minimizedNodes[i]:(n.child._str=i,this.minimizedNodes[i]=n.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(r){var n=new t.QueryParser(e,r);n.parse()})},t.Index.prototype.query=function(e){for(var r=new t.Query(this.fields),n=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,r){var n=e[this._ref],i=Object.keys(this._fields);this._documents[n]=r||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,r;do e=this.next(),r=e.charCodeAt(0);while(r>47&&r<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var r=e.next();if(r==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(r.charCodeAt(0)==92){e.escapeCharacter();continue}if(r==":")return t.QueryLexer.lexField;if(r=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(r=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(r=="+"&&e.width()===1||r=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(r.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,r){this.lexer=new t.QueryLexer(e),this.query=r,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var r=e.peekLexeme();if(r!=null)switch(r.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expected either a field or a term, found "+r.type;throw r.str.length>=1&&(n+=" with value '"+r.str+"'"),new t.QueryParseError(n,r.start,r.end)}},t.QueryParser.parsePresence=function(e){var r=e.consumeLexeme();if(r!=null){switch(r.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var n="unrecognised presence operator'"+r.str+"'";throw new t.QueryParseError(n,r.start,r.end)}var i=e.peekLexeme();if(i==null){var n="expecting term or field, found nothing";throw new t.QueryParseError(n,r.start,r.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(n,i.start,i.end)}}},t.QueryParser.parseField=function(e){var r=e.consumeLexeme();if(r!=null){if(e.query.allFields.indexOf(r.str)==-1){var n=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+r.str+"', possible fields: "+n;throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.fields=[r.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,r.start,r.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var r=e.consumeLexeme();if(r!=null){e.currentClause.term=r.str.toLowerCase(),r.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var n=e.peekLexeme();if(n==null){e.nextClause();return}switch(n.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+n.type+"'";throw new t.QueryParseError(i,n.start,n.end)}}},t.QueryParser.parseEditDistance=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="edit distance must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.editDistance=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="boost must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.boost=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,r){typeof define=="function"&&define.amd?define(r):typeof G=="object"?J.exports=r():e.lunr=r()}(this,function(){return t})})()});var K=q((we,Z)=>{"use strict";var me=/["'&<>]/;Z.exports=ve;function ve(t){var e=""+t,r=me.exec(e);if(!r)return e;var n,i="",s=0,o=0;for(s=r.index;s`${i}${s}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${t.separator})(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(e,"|")})`,"img");return s=>s.replace(i,r).replace(/<\/mark>(\s+)]*>/img,"$1")}}function ne(t){let e=new lunr.Query(["title","text"]);return new lunr.QueryParser(t,e).parse(),e.clauses}function ie(t,e){let r=new Set(t),n={};for(let i=0;i!n.has(i)))]}var W=class{constructor({config:e,docs:r,pipeline:n,index:i}){this.documents=te(r),this.highlight=re(e),lunr.tokenizer.separator=new RegExp(e.separator),typeof i=="undefined"?this.index=lunr(function(){e.lang.length===1&&e.lang[0]!=="en"?this.use(lunr[e.lang[0]]):e.lang.length>1&&this.use(lunr.multiLanguage(...e.lang));let s=xe(["trimmer","stopWordFilter","stemmer"],n);for(let o of e.lang.map(a=>a==="en"?lunr:lunr[a]))for(let a of s)this.pipeline.remove(o[a]),this.searchPipeline.remove(o[a]);this.field("title",{boost:1e3}),this.field("text"),this.ref("location");for(let o of r)this.add(o)}):this.index=lunr.Index.load(i)}search(e){if(e)try{let r=this.highlight(e),n=ne(e).filter(s=>s.presence!==lunr.Query.presence.PROHIBITED);return[...this.index.search(`${e}*`).reduce((s,{ref:o,score:a,matchData:u})=>{let c=this.documents.get(o);if(typeof c!="undefined"){let{location:d,title:y,text:p,parent:b}=c,m=ie(n,Object.keys(u.metadata)),Q=+!b+ +Object.values(m).every(f=>f);s.push({location:d,title:r(y),text:r(p),score:a*(1+Q),terms:m})}return s},[]).sort((s,o)=>o.score-s.score).reduce((s,o)=>{let a=this.documents.get(o.location);if(typeof a!="undefined"){let u="parent"in a?a.parent.location:a.location;s.set(u,[...s.get(u)||[],o])}return s},new Map).values()]}catch(r){console.warn(`Invalid query: ${e} \u2013 see https://bit.ly/2s3ChXG`)}return[]}};var R;(function(i){i[i.SETUP=0]="SETUP",i[i.READY=1]="READY",i[i.QUERY=2]="QUERY",i[i.RESULT=3]="RESULT"})(R||(R={}));var H;function Se(t){return z(this,null,function*(){let e="../lunr";if(typeof parent!="undefined"&&"IFrameWorker"in parent){let n=document.querySelector("script[src]"),[i]=n.src.split("/worker");e=e.replace("..",i)}let r=[];for(let n of t.lang)n==="ja"&&r.push(`${e}/tinyseg.js`),n!=="en"&&r.push(`${e}/min/lunr.${n}.min.js`);t.lang.length>1&&r.push(`${e}/min/lunr.multi.min.js`),r.length&&(yield importScripts(`${e}/min/lunr.stemmer.support.min.js`,...r))})}function Qe(t){return z(this,null,function*(){switch(t.type){case R.SETUP:return yield Se(t.data.config),H=new W(t.data),{type:R.READY};case R.QUERY:return{type:R.RESULT,data:H?H.search(t.data):[]};default:throw new TypeError("Invalid message type")}})}self.lunr=se.default;addEventListener("message",t=>z(void 0,null,function*(){postMessage(yield Qe(t.data))}));})(); +/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */ +/*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ +/*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + */ +/*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + */ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */ +//# sourceMappingURL=search.d351de03.min.js.map + diff --git a/v10.0/assets/logo/dmo-logo-white.min.svg b/v10.0/assets/logo/dmo-logo-white.min.svg new file mode 100644 index 00000000..7ec0dbb3 --- /dev/null +++ b/v10.0/assets/logo/dmo-logo-white.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v10.0/assets/logo/dmo-logo.min.svg b/v10.0/assets/logo/dmo-logo.min.svg new file mode 100644 index 00000000..72c98a5a --- /dev/null +++ b/v10.0/assets/logo/dmo-logo.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v10.0/assets/logo/favicon-32x32.png b/v10.0/assets/logo/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..f129fc449c1ae90b39f32da80e214e28e8dfc26c GIT binary patch literal 1130 zcmV-w1eN=VP)5r;fx)y$f4it-@ka^JE;gxu!Q z>0+nTF@7!^6XzYX!i2p}gg#C~z-`VC;Wn?JgWNM!k8;_?6a$pY@?`}GwC{R2=rP0d zcJ|30jGxWIM4tmUzT5|Yy$b*PD*P^iz=ykMg&FhM_7Fly><(=Us9z0w8(d+8&vpPBmy7VFxF}BAcPQ0s@%mC1EOgNNx7wnbVEEz zxh{zin-=dvhgmc?fWOt44_(-yK$BKQ2qCdtw=u~5pO>LrWl}#yhKy= z$H_zS>itv^%1mcx8|mG(wuzov8+vMO=&rS)t5QSPk;VPh%UZDgdA(_WhMqoMi_3DR z+8|xSkgWj-*Dyr$HKe?7IugPvjuXaZGgAz#i&Yy^%3P3bA*l6NV6Ey#QqfT`%?8S4 zXG~1Yy@Yb3DJi!MqVdl)w`?APacT_en?Hidy^&B{{%YZ(m=F>bIVOrcmXKWnC@wNQKA3+=6+Rt1->0qiVwfhopK3l}L1PJCRd z*v1qC%4_GR7pm2LV2XjJ58TjC{C0=%AW~mE1E$(w;quuf&plI&ACd|yA=^Tc0LS~m z6a$GyI`q21mAAdckCgIH!SpmxF6(Z4@i40_VVVi0#(V1*yzwoxw|`#QuG>P;PYxmZ z=>{-$H<%*dogTKOB{nfG`x7z_K|L`z-HDYA+coeSFA{SOgK>F-T4m`nz_+w5MQo+q zxSWu22-fO;2-nDMgUHq(^piv2H9jO2zY3H&Ne3&C z`qEjbb$>u>xC*`TZ|IGGL2I}Q^}S259_vN&;YK7Lcq-CvZv0NUcw)&rewm84aB+|- z#sK9q%rq16>G?>=DndeLA>z{l wLapM6g^On>7Y>V6#1cZH*BrW`4c%@13!ImS;1L_K4gdfE07*qoM6N<$f;%P|Pyhe` literal 0 HcmV?d00001 diff --git a/v10.0/assets/stylesheets/main.875de78c.min.css b/v10.0/assets/stylesheets/main.875de78c.min.css new file mode 100644 index 00000000..4e51d528 --- /dev/null +++ b/v10.0/assets/stylesheets/main.875de78c.min.css @@ -0,0 +1,2 @@ +@charset "UTF-8";html{-webkit-text-size-adjust:none;-ms-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:content-box;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:separate;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:transparent;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root{--md-default-fg-color:rgba(0,0,0,0.87);--md-default-fg-color--light:rgba(0,0,0,0.54);--md-default-fg-color--lighter:rgba(0,0,0,0.32);--md-default-fg-color--lightest:rgba(0,0,0,0.07);--md-default-bg-color:#fff;--md-default-bg-color--light:hsla(0,0%,100%,0.7);--md-default-bg-color--lighter:hsla(0,0%,100%,0.3);--md-default-bg-color--lightest:hsla(0,0%,100%,0.12);--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7);--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:rgba(82,108,254,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}:root>*{--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-hl-color:rgba(255,255,0,0.5);--md-code-hl-number-color:#d52a2a;--md-code-hl-special-color:#db1457;--md-code-hl-function-color:#a846b9;--md-code-hl-constant-color:#6e59d9;--md-code-hl-keyword-color:#3f6ec6;--md-code-hl-string-color:#1c7d4d;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-mark-color:rgba(255,255,0,0.5);--md-typeset-del-color:rgba(245,80,61,0.15);--md-typeset-ins-color:rgba(11,213,112,0.15);--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-footer-fg-color:#fff;--md-footer-fg-color--light:hsla(0,0%,100%,0.7);--md-footer-fg-color--lighter:hsla(0,0%,100%,0.3);--md-footer-bg-color:rgba(0,0,0,0.87);--md-footer-bg-color--dark:rgba(0,0,0,0.32)}.md-icon svg{fill:currentColor;display:block;height:1.2rem;width:1.2rem}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body,input{font-feature-settings:"kern","liga";font-family:var(--md-text-font-family,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif}body,code,input,kbd,pre{color:var(--md-typeset-color)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family,_),SFMono-Regular,Consolas,Menlo,monospace}:root{--md-typeset-table--ascending:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table--descending:url('data:image/svg+xml;charset=utf-8,')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.8rem;line-height:1.6}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin:1em 0}.md-typeset h1{color:var(--md-default-fg-color--light);font-size:2em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:300;letter-spacing:-.01em}.md-typeset h2{font-size:1.5625em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:400;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.1rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:0 .2941176471em;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset h1 code,.md-typeset h2 code,.md-typeset h3 code,.md-typeset h4 code,.md-typeset h5 code,.md-typeset h6 code{background-color:transparent;box-shadow:none;margin:initial;padding:initial}.md-typeset a code{color:currentColor}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) transparent;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@media screen and (max-width:44.9375em){.md-typeset>pre{margin:1em -.8rem}.md-typeset>pre code{border-radius:0}}.md-typeset kbd{background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -.1rem .2rem var(--md-typeset-kbd-accent-color) inset;color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help;text-decoration:none}@media (hover:none){.md-typeset abbr{position:relative}.md-typeset abbr[title]:focus:after,.md-typeset abbr[title]:hover:after{background-color:var(--md-default-fg-color);border-radius:.1rem;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);color:var(--md-default-bg-color);content:attr(title);display:inline-block;font-size:.7rem;left:0;margin-top:2em;max-width:80%;min-width:-webkit-max-content;min-width:-moz-max-content;min-width:max-content;padding:.2rem .3rem;position:absolute;width:auto}}.md-typeset small{opacity:.75}.md-typeset sub,.md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-left:0;margin-right:.078125em}.md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter);color:var(--md-default-fg-color--light);display:flow-root;padding-left:.6rem}[dir=rtl] .md-typeset blockquote{border-left:initial;border-right:.2rem solid var(--md-default-fg-color--lighter);padding-left:0;padding-right:.6rem}.md-typeset ul{list-style-type:disc}.md-typeset ol,.md-typeset ul{display:flow-root;margin-left:.625em;padding:0}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-left:0;margin-right:.625em}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em;margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-left:0;margin-right:1.25em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}.md-typeset ol li ol,.md-typeset ol li ul,.md-typeset ul li ol,.md-typeset ul li ul{margin:.5em 0 .5em .625em}[dir=rtl] .md-typeset ol li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ul li ul{margin-left:0;margin-right:.625em}.md-typeset dd{margin:1em 0 1.5em 1.875em}[dir=rtl] .md-typeset dd{margin-left:0;margin-right:1.875em}.md-typeset img,.md-typeset svg{height:auto;max-width:100%}.md-typeset img[align=left],.md-typeset svg[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right],.md-typeset svg[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child,.md-typeset svg[align]:only-child{margin-top:0}.md-typeset figure{display:flow-root;margin:0 auto;max-width:100%;text-align:center;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.md-typeset figure img{display:block}.md-typeset figcaption{font-style:italic;margin:1em auto 2em;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .05rem rgba(0,0,0,.1);display:inline-block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) td>:first-child,.md-typeset table:not([class]) th>:first-child{margin-top:0}.md-typeset table:not([class]) td>:last-child,.md-typeset table:not([class]) th>:last-child{margin-bottom:0}.md-typeset table:not([class]) td:not([align]),.md-typeset table:not([class]) th:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) td:not([align]),[dir=rtl] .md-typeset table:not([class]) th:not([align]){text-align:right}.md-typeset table:not([class]) th{background-color:var(--md-default-fg-color--light);color:var(--md-default-bg-color);min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) th a{color:inherit}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-default-fg-color--lightest);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tr{transition:background-color 125ms}.md-typeset table:not([class]) tr:hover{background-color:rgba(0,0,0,.04);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) tr:first-child td{border-top:0}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;margin-left:.5em;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:sub;width:1.2em}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:currentColor;-webkit-mask-image:var(--md-typeset-table--ascending);mask-image:var(--md-typeset-table--ascending)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:currentColor;-webkit-mask-image:var(--md-typeset-table--descending);mask-image:var(--md-typeset-table--descending)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}html{font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.9375em){body[data-md-state=lock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}.md-announce{background-color:var(--md-footer-bg-color);overflow:auto}@media print{.md-announce{display:none}}.md-announce__inner{color:var(--md-footer-fg-color);font-size:.7rem;margin:.6rem auto;padding:0 .8rem}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8,')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;position:absolute;right:.5em;top:.5em;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:focus,.md-clipboard:hover{color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentColor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:focus code,.md-clipboard--inline:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-content{flex-grow:1;overflow:hidden;scroll-padding-top:51.2rem}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width:76.25em){.md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:.8rem;margin-right:1.2rem}.md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem;margin-right:.8rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}.md-content__button{float:right;margin:.4rem 0 .4rem .4rem;padding:0}@media print{.md-content__button{display:none}}[dir=rtl] .md-content__button{float:left;margin-left:0;margin-right:.4rem}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}.md-dialog{background-color:var(--md-default-fg-color);border-radius:.1rem;bottom:.8rem;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);left:auto;min-width:11.1rem;opacity:0;padding:.4rem .6rem;pointer-events:none;position:fixed;right:.8rem;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:2}@media print{.md-dialog{display:none}}[dir=rtl] .md-dialog{left:.8rem;right:auto}.md-dialog[data-md-state=open]{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-typeset .md-button{border:.1rem solid;border-radius:.1rem;color:var(--md-primary-fg-color);display:inline-block;font-weight:700;padding:.625em 2em;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button:focus,.md-typeset .md-button:hover{background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-typeset .md-input{border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.1),0 .025rem .05rem rgba(0,0,0,.1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:box-shadow .25s}.md-typeset .md-input:focus,.md-typeset .md-input:hover{box-shadow:0 .4rem 1rem rgba(0,0,0,.15),0 .025rem .05rem rgba(0,0,0,.15)}.md-typeset .md-input--stretch{width:100%}.md-header{background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem transparent,0 .2rem .4rem transparent;color:var(--md-primary-bg-color);left:0;position:-webkit-sticky;position:sticky;right:0;top:0;z-index:2}@media print{.md-header{display:none}}.md-header[data-md-state=shadow]{box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);transition:transform .25s cubic-bezier(.1,.7,.1,1),box-shadow .25s}.md-header[data-md-state=hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1),box-shadow .25s}.md-header__inner{align-items:center;display:flex;padding:0 .2rem}.md-header__button{color:currentColor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.1875em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo img,.md-header__button.md-logo svg{fill:currentColor;display:block;height:1.2rem;width:1.2rem}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;line-height:2.4rem;margin-left:1rem;margin-right:.4rem}.md-header__title[data-md-state=active] .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title[data-md-state=active] .md-header__topic{transform:translateX(1.25rem)}.md-header__title[data-md-state=active] .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;transition:max-width 0ms .25s,opacity .25s .25s;white-space:nowrap}[data-md-toggle=search]:checked~.md-header .md-header__option{max-width:0;opacity:0;transition:max-width 0ms,opacity 0ms}.md-header__source{display:none}@media screen and (min-width:60em){.md-header__source{display:block;margin-left:1rem;max-width:11.7rem;width:11.7rem}[dir=rtl] .md-header__source{margin-left:0;margin-right:1rem}}@media screen and (min-width:76.25em){.md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-footer{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{overflow:auto;padding:.2rem}.md-footer__link{display:flex;outline-color:var(--md-accent-fg-color);padding-bottom:.4rem;padding-top:1.4rem;transition:opacity .25s}@media screen and (min-width:45em){.md-footer__link{width:50%}}.md-footer__link:focus,.md-footer__link:hover{opacity:.7}.md-footer__link--prev{float:left}@media screen and (max-width:44.9375em){.md-footer__link--prev{width:25%}.md-footer__link--prev .md-footer__title{display:none}}[dir=rtl] .md-footer__link--prev{float:right}[dir=rtl] .md-footer__link--prev svg{transform:scaleX(-1)}.md-footer__link--next{float:right;text-align:right}@media screen and (max-width:44.9375em){.md-footer__link--next{width:75%}}[dir=rtl] .md-footer__link--next{float:left;text-align:left}[dir=rtl] .md-footer__link--next svg{transform:scaleX(-1)}.md-footer__title{flex-grow:1;font-size:.9rem;line-height:2.4rem;max-width:calc(100% - 2.4rem);padding:0 1rem;position:relative}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.64rem;left:0;margin-top:-1rem;opacity:.7;padding:0 1rem;position:absolute;right:0}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:focus,html .md-footer-meta.md-typeset a:hover{color:var(--md-footer-fg-color)}.md-footer-copyright{color:var(--md-footer-fg-color--lighter);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-footer-copyright{width:auto}}.md-footer-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-footer-social{margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-footer-social{padding:.6rem 0}}.md-footer-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-footer-social__link:before{line-height:1.9}.md-footer-social__link svg{fill:currentColor;max-height:.8rem;vertical-align:-25%}:root{--md-nav-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8,');--md-toc-icon:url('data:image/svg+xml;charset=utf-8,')}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{display:block;font-weight:700;overflow:hidden;padding:0 .6rem;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{height:100%;width:auto}.md-nav__title .md-nav__button.md-logo img,.md-nav__title .md-nav__button.md-logo svg{fill:currentColor;display:block;height:2.4rem;width:2.4rem}.md-nav__list{list-style:none;margin:0;padding:0}.md-nav__item{padding:0 .6rem}.md-nav__item .md-nav__item{padding-right:0}[dir=rtl] .md-nav__item .md-nav__item{padding-left:0;padding-right:.6rem}.md-nav__link{cursor:pointer;display:block;margin-top:.625em;overflow:hidden;scroll-snap-align:start;text-overflow:ellipsis;transition:color 125ms}.md-nav__link[data-md-state=blur]{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active{color:var(--md-typeset-a-color)}.md-nav__item--nested>.md-nav__link{color:inherit}.md-nav__link:focus,.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-nav--primary .md-nav__link[for=__toc]{display:none}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{background-color:currentColor;display:block;height:100%;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);width:100%}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__source{display:none}@media screen and (max-width:76.1875em){.md-nav--primary,.md-nav--primary .md-nav{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;height:100%;left:0;position:absolute;right:0;top:0;z-index:1}.md-nav--primary .md-nav__item,.md-nav--primary .md-nav__title{font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);cursor:pointer;font-weight:400;height:5.6rem;line-height:2.4rem;padding:3rem .8rem .2rem;position:relative;white-space:nowrap}.md-nav--primary .md-nav__title .md-nav__icon{display:block;height:1.2rem;left:.4rem;margin:.2rem;position:absolute;top:.4rem;width:1.2rem}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{left:auto;right:.4rem}.md-nav--primary .md-nav__title .md-nav__icon:after{background-color:currentColor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--prev);mask-image:var(--md-nav-icon--prev);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}.md-nav--primary .md-nav__title~.md-nav__list{background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;overflow-y:auto;-ms-scroll-snap-type:y mandatory;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-nav--primary .md-nav__title .md-logo{display:block;left:.2rem;margin:.2rem;padding:.4rem;position:absolute;top:.2rem}[dir=rtl] .md-nav--primary .md-nav__title .md-logo{left:auto;right:.2rem}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{border-top:.05rem solid var(--md-default-fg-color--lightest);padding:0}.md-nav--primary .md-nav__item--nested>.md-nav__link{padding-right:2.4rem}[dir=rtl] .md-nav--primary .md-nav__item--nested>.md-nav__link{padding-left:2.4rem;padding-right:.8rem}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:focus,.md-nav--primary .md-nav__item--active>.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{margin-top:0;padding:.6rem .8rem;position:relative}.md-nav--primary .md-nav__link .md-nav__icon{color:inherit;font-size:1.2rem;height:1.2rem;margin-top:-.6rem;position:absolute;right:.6rem;top:50%;width:1.2rem}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{left:.6rem;right:auto}.md-nav--primary .md-nav__link .md-nav__icon:after{background-color:currentColor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-nav--primary .md-nav__icon:after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav__link{position:static}.md-nav--primary .md-nav--secondary .md-nav{background-color:transparent;position:static}.md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:0;padding-right:1.4rem}.md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:0;padding-right:2rem}.md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:0;padding-right:2.6rem}.md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:0;padding-right:3.2rem}.md-nav--secondary{background-color:transparent}.md-nav__toggle~.md-nav{display:flex;opacity:0;transform:translateX(100%);transition:transform .25s cubic-bezier(.8,0,.6,1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{opacity:1;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{-webkit-backface-visibility:hidden;backface-visibility:hidden}}@media screen and (max-width:59.9375em){.md-nav--primary .md-nav__link[for=__toc]{display:block;padding-right:2.4rem}[dir=rtl] .md-nav--primary .md-nav__link[for=__toc]{padding-left:2.4rem;padding-right:.8rem}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--primary .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:flex}.md-nav__source{background-color:var(--md-primary-fg-color--dark);color:var(--md-primary-bg-color);display:block;padding:0 .2rem}}@media screen and (min-width:60em) and (max-width:76.1875em){.md-nav--integrated .md-nav__link[for=__toc]{display:block;padding-right:2.4rem;scroll-snap-align:none}[dir=rtl] .md-nav--integrated .md-nav__link[for=__toc]{padding-left:2.4rem;padding-right:.8rem}.md-nav--integrated .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--integrated .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav{display:flex}}@media screen and (min-width:60em){.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}}@media screen and (min-width:76.25em){.md-nav{transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon{display:none}.md-nav__toggle~.md-nav{display:none}.md-nav__toggle:checked~.md-nav,.md-nav__toggle:indeterminate~.md-nav{display:block}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__item--section{display:block;margin:1.25em 0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{display:none}.md-nav__item--section>.md-nav{display:block}.md-nav__item--section>.md-nav>.md-nav__title{display:block;padding:0;pointer-events:none;scroll-snap-align:start}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav__icon{float:right;height:.9rem;transition:transform .25s;width:.9rem}[dir=rtl] .md-nav__icon{float:left;transform:rotate(180deg)}.md-nav__icon:after{background-color:currentColor;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:-.1rem;width:100%}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon,.md-nav__item--nested .md-nav__toggle:indeterminate~.md-nav__link .md-nav__icon{transform:rotate(90deg)}.md-nav--lifted>.md-nav__list>.md-nav__item--nested,.md-nav--lifted>.md-nav__title{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block;padding:0}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav>.md-nav__title{display:block;padding:0 .6rem;pointer-events:none;scroll-snap-align:start}.md-nav--lifted .md-nav[data-md-level="1"]{display:block}.md-nav--lifted .md-nav[data-md-level="1"]>.md-nav__list>.md-nav__item{padding-right:.6rem}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav{border-left:.05rem solid var(--md-primary-fg-color);display:block;margin-bottom:1.25em}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav>.md-nav__title{display:none}}:root{--md-search-result-icon:url('data:image/svg+xml;charset=utf-8,')}.md-search{position:relative}@media screen and (min-width:60em){.md-search{padding:.2rem 0}}.no-js .md-search{display:none}.md-search__overlay{opacity:0;z-index:1}@media screen and (max-width:59.9375em){.md-search__overlay{background-color:var(--md-default-bg-color);border-radius:1rem;height:2rem;left:-2.2rem;overflow:hidden;pointer-events:none;position:absolute;top:.2rem;transform-origin:center;transition:transform .3s .1s,opacity .2s .2s;width:2rem}[dir=rtl] .md-search__overlay{left:auto;right:-2.2rem}[data-md-toggle=search]:checked~.md-header .md-search__overlay{opacity:1;transition:transform .4s,opacity .1s}}@media screen and (min-width:60em){.md-search__overlay{background-color:rgba(0,0,0,.54);cursor:pointer;height:0;left:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0}[dir=rtl] .md-search__overlay{left:auto;right:0}[data-md-toggle=search]:checked~.md-header .md-search__overlay{height:200vh;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@media screen and (max-width:29.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(45)}}@media screen and (min-width:30em) and (max-width:44.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(60)}}@media screen and (min-width:45em) and (max-width:59.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(75)}}.md-search__inner{-webkit-backface-visibility:hidden;backface-visibility:hidden}@media screen and (max-width:59.9375em){.md-search__inner{height:100%;left:100%;opacity:0;position:fixed;top:0;transform:translateX(5%);transition:right 0ms .3s,left 0ms .3s,transform .15s cubic-bezier(.4,0,.2,1) .15s,opacity .15s .15s;width:100%;z-index:2}[data-md-toggle=search]:checked~.md-header .md-search__inner{left:0;opacity:1;transform:translateX(0);transition:right 0ms 0ms,left 0ms 0ms,transform .15s cubic-bezier(.1,.7,.1,1) .15s,opacity .15s .15s}[dir=rtl] [data-md-toggle=search]:checked~.md-header .md-search__inner{left:auto;right:0}html [dir=rtl] .md-search__inner{left:auto;right:100%;transform:translateX(-5%)}}@media screen and (min-width:60em){.md-search__inner{float:right;padding:.1rem 0;position:relative;transition:width .25s cubic-bezier(.1,.7,.1,1);width:11.7rem}[dir=rtl] .md-search__inner{float:left}}@media screen and (min-width:60em) and (max-width:76.1875em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:23.4rem}}@media screen and (min-width:76.25em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:34.4rem}}.md-search__form{position:relative}@media screen and (min-width:60em){.md-search__form{border-radius:.1rem}}.md-search__input{background-color:var(--md-default-bg-color);box-shadow:0 0 .6rem transparent;padding:0 2.2rem 0 3.6rem;position:relative;text-overflow:ellipsis;transition:color .25s,background-color .25s,box-shadow .25s;z-index:2}[dir=rtl] .md-search__input{padding:0 3.6rem 0 2.2rem}.md-search__input::-webkit-input-placeholder{-webkit-transition:color .25s;transition:color .25s}.md-search__input::-moz-placeholder{-moz-transition:color .25s;transition:color .25s}.md-search__input::-ms-input-placeholder{-ms-transition:color .25s;transition:color .25s}.md-search__input::placeholder{transition:color .25s}.md-search__input::-webkit-input-placeholder{color:var(--md-default-fg-color--light)}.md-search__input::-moz-placeholder{color:var(--md-default-fg-color--light)}.md-search__input::-ms-input-placeholder{color:var(--md-default-fg-color--light)}.md-search__input::placeholder,.md-search__input~.md-search__icon{color:var(--md-default-fg-color--light)}.md-search__input::-ms-clear{display:none}[data-md-toggle=search]:checked~.md-header .md-search__input{box-shadow:0 0 .6rem rgba(0,0,0,.07)}@media screen and (max-width:59.9375em){.md-search__input{font-size:.9rem;height:2.4rem;width:100%}}@media screen and (min-width:60em){.md-search__input{background-color:rgba(0,0,0,.26);border-radius:.1rem;color:inherit;font-size:.8rem;height:1.8rem;padding-left:2.2rem;width:100%}[dir=rtl] .md-search__input{padding-right:2.2rem}.md-search__input+.md-search__icon{color:var(--md-primary-bg-color)}.md-search__input::-webkit-input-placeholder{color:var(--md-primary-bg-color--light)}.md-search__input::-moz-placeholder{color:var(--md-primary-bg-color--light)}.md-search__input::-ms-input-placeholder{color:var(--md-primary-bg-color--light)}.md-search__input::placeholder{color:var(--md-primary-bg-color--light)}.md-search__input:hover{background-color:hsla(0,0%,100%,.12)}[data-md-toggle=search]:checked~.md-header .md-search__input{background-color:var(--md-default-bg-color);border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color);text-overflow:clip}[data-md-toggle=search]:checked~.md-header .md-search__input::-webkit-input-placeholder{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input::-moz-placeholder{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input::-ms-input-placeholder{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input+.md-search__icon,[data-md-toggle=search]:checked~.md-header .md-search__input::placeholder{color:var(--md-default-fg-color--light)}}.md-search__icon{cursor:pointer;height:1.2rem;position:absolute;transition:color .25s,opacity .25s;width:1.2rem;z-index:2}.md-search__icon:hover{opacity:.7}.md-search__icon[for=__search]{left:.5rem;top:.3rem}[dir=rtl] .md-search__icon[for=__search]{left:auto;right:.5rem}[dir=rtl] .md-search__icon[for=__search] svg{transform:scaleX(-1)}@media screen and (max-width:59.9375em){.md-search__icon[for=__search]{left:.8rem;top:.6rem}[dir=rtl] .md-search__icon[for=__search]{left:auto;right:.8rem}.md-search__icon[for=__search] svg:first-child{display:none}}@media screen and (min-width:60em){.md-search__icon[for=__search]{pointer-events:none}.md-search__icon[for=__search] svg:last-child{display:none}}.md-search__icon[type=reset]{opacity:0;pointer-events:none;right:.5rem;top:.3rem;transform:scale(.75);transition:transform .15s cubic-bezier(.1,.7,.1,1),opacity .15s}[dir=rtl] .md-search__icon[type=reset]{left:.5rem;right:auto}@media screen and (max-width:59.9375em){.md-search__icon[type=reset]{right:.8rem;top:.6rem}[dir=rtl] .md-search__icon[type=reset]{left:.8rem;right:auto}}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__icon[type=reset]{opacity:1;pointer-events:auto;transform:scale(1)}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__icon[type=reset]:hover{opacity:.7}.md-search__output{border-radius:0 0 .1rem .1rem;overflow:hidden;position:absolute;width:100%;z-index:1}@media screen and (max-width:59.9375em){.md-search__output{bottom:0;top:2.4rem}}@media screen and (min-width:60em){.md-search__output{opacity:0;top:1.9rem;transition:opacity .4s}[data-md-toggle=search]:checked~.md-header .md-search__output{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.4);opacity:1}}.md-search__scrollwrap{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);height:100%;overflow-y:auto;touch-action:pan-y}@media (-webkit-max-device-pixel-ratio:1),(max-resolution:1dppx){.md-search__scrollwrap{transform:translateZ(0)}}@media screen and (min-width:60em) and (max-width:76.1875em){.md-search__scrollwrap{width:23.4rem}}@media screen and (min-width:76.25em){.md-search__scrollwrap{width:34.4rem}}@media screen and (min-width:60em){.md-search__scrollwrap{max-height:0;scrollbar-color:var(--md-default-fg-color--lighter) transparent;scrollbar-width:thin}[data-md-toggle=search]:checked~.md-header .md-search__scrollwrap{max-height:75vh}.md-search__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-search__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-search__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-search__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}}.md-search-result{color:var(--md-default-fg-color);word-break:break-word}.md-search-result__meta{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.8rem;padding:0 .8rem;scroll-snap-align:start}@media screen and (min-width:60em){.md-search-result__meta{padding-left:2.2rem}[dir=rtl] .md-search-result__meta{padding-left:0;padding-right:2.2rem}}.md-search-result__list{list-style:none;margin:0;padding:0}.md-search-result__item{box-shadow:0 -.05rem 0 var(--md-default-fg-color--lightest)}.md-search-result__item:first-child{box-shadow:none}.md-search-result__link{display:block;outline:none;scroll-snap-align:start;transition:background-color .25s}.md-search-result__link:focus,.md-search-result__link:hover{background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:last-child p:last-child{margin-bottom:.6rem}.md-search-result__more summary{color:var(--md-typeset-a-color);cursor:pointer;display:block;font-size:.64rem;outline:none;padding:.75em .8rem;scroll-snap-align:start;transition:color .25s,background-color .25s}@media screen and (min-width:60em){.md-search-result__more summary{padding-left:2.2rem}[dir=rtl] .md-search-result__more summary{padding-left:.8rem;padding-right:2.2rem}}.md-search-result__more summary:focus,.md-search-result__more summary:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more summary::-webkit-details-marker,.md-search-result__more summary::marker{display:none}.md-search-result__more summary~*>*{opacity:.65}.md-search-result__article{overflow:hidden;padding:0 .8rem;position:relative}@media screen and (min-width:60em){.md-search-result__article{padding-left:2.2rem}[dir=rtl] .md-search-result__article{padding-left:.8rem;padding-right:2.2rem}}.md-search-result__article--document .md-search-result__title{font-size:.8rem;font-weight:400;line-height:1.4;margin:.55rem 0}.md-search-result__icon{color:var(--md-default-fg-color--light);height:1.2rem;left:0;margin:.5rem;position:absolute;width:1.2rem}@media screen and (max-width:59.9375em){.md-search-result__icon{display:none}}.md-search-result__icon:after{background-color:currentColor;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-search-result-icon);mask-image:var(--md-search-result-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-search-result__icon{left:auto;right:0}[dir=rtl] .md-search-result__icon:after{transform:scaleX(-1)}.md-search-result__title{font-size:.64rem;font-weight:700;line-height:1.6;margin:.5em 0}.md-search-result__teaser{-webkit-box-orient:vertical;-webkit-line-clamp:2;color:var(--md-default-fg-color--light);display:-webkit-box;font-size:.64rem;line-height:1.6;margin:.5em 0;max-height:2rem;overflow:hidden;text-overflow:ellipsis}@media screen and (max-width:44.9375em){.md-search-result__teaser{-webkit-line-clamp:3;max-height:3rem}}@media screen and (min-width:60em) and (max-width:76.1875em){.md-search-result__teaser{-webkit-line-clamp:3;max-height:3rem}}.md-search-result__teaser mark{background-color:transparent;text-decoration:underline}.md-search-result__terms{font-size:.64rem;font-style:italic;margin:.5em 0}.md-search-result mark{background-color:transparent;color:var(--md-accent-fg-color)}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.1),0 0 .05rem rgba(0,0,0,.25);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select__inner:after{border-bottom:.2rem solid transparent;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid transparent;border-right:.2rem solid transparent;border-top:0;content:"";height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}.md-select__link{cursor:pointer;display:block;outline:none;padding-left:.6rem;padding-right:1.2rem;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link:focus,.md-select__link:hover{color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.2rem 0;position:-webkit-sticky;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.1875em){.md-sidebar--primary{background-color:var(--md-default-bg-color);display:block;height:100%;left:-12.1rem;position:fixed;top:0;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s;width:12.1rem;z-index:3}[dir=rtl] .md-sidebar--primary{left:auto;right:-12.1rem}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.4);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overflow:hidden;position:absolute;right:0;-ms-scroll-snap-type:none;scroll-snap-type:none;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{-webkit-backface-visibility:hidden;backface-visibility:hidden;margin:0 .2rem;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) transparent;scrollbar-width:thin}.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@media screen and (max-width:76.1875em){.md-overlay{background-color:rgba(0,0,0,.54);height:0;opacity:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0;z-index:3}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@-webkit-keyframes facts{0%{height:0}to{height:.65rem}}@keyframes facts{0%{height:0}to{height:.65rem}}@-webkit-keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-source{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;font-size:.65rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}.md-source__icon svg{margin-left:.6rem;margin-top:.6rem}[dir=rtl] .md-source__icon svg{margin-left:0;margin-right:.6rem}.md-source__icon+.md-source__repository{margin-left:-2rem;padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-left:0;margin-right:-2rem;padding-left:0;padding-right:2rem}.md-source__repository{display:inline-block;margin-left:.6rem;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{font-size:.55rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0}[data-md-state=done] .md-source__facts{-webkit-animation:facts .25s ease-in;animation:facts .25s ease-in}.md-source__fact{display:inline-block}[data-md-state=done] .md-source__fact{-webkit-animation:fact .4s ease-out;animation:fact .4s ease-out}.md-source__fact:before{background-color:currentColor;content:"";display:inline-block;height:.6rem;margin-right:.1rem;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}.md-source__fact:nth-child(1n+2):before{margin-left:.4rem}[dir=rtl] .md-source__fact{margin-left:.1rem;margin-right:0}[dir=rtl] .md-source__fact:nth-child(1n+2):before{margin-left:0;margin-right:.4rem}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}.md-tabs{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);overflow:auto;width:100%}@media print{.md-tabs{display:none}}@media screen and (max-width:76.1875em){.md-tabs{display:none}}.md-tabs[data-md-state=hidden]{pointer-events:none}.md-tabs__list{contain:content;list-style:none;margin:0 0 0 .2rem;padding:0;white-space:nowrap}[dir=rtl] .md-tabs__list{margin-left:0;margin-right:.2rem}.md-tabs__item{display:inline-block;height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__link{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link--active,.md-tabs__link:focus,.md-tabs__link:hover{color:inherit;opacity:1}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[data-md-state=hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}.md-top{background:var(--md-primary-fg-color);border-radius:100%;bottom:.4rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.1),0 .025rem .05rem rgba(0,0,0,.1);color:var(--md-primary-bg-color);float:right;margin:-2.8rem .4rem .4rem;outline:none;padding:.4rem;position:-webkit-sticky;position:sticky;transform:translateY(0);transition:opacity 125ms,transform 125ms cubic-bezier(.4,0,.2,1),background-color 125ms;z-index:1}[dir=rtl] .md-top{float:left}.md-top[data-md-state=hidden]{opacity:0;pointer-events:none;transform:translateY(-.2rem)}.md-top:focus,.md-top:hover{background:var(--md-accent-fg-color);transform:scale(1.1)}@-webkit-keyframes hoverfix{0%{pointer-events:none}}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}.md-version__current{color:inherit;cursor:pointer;margin-left:1.4rem;margin-right:.4rem;outline:none;position:relative;top:.05rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current:after{background-color:currentColor;content:"";display:inline-block;height:.6rem;margin-left:.4rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;width:.4rem}[dir=rtl] .md-version__current:after{margin-left:0;margin-right:.4rem}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.1),0 0 .05rem rgba(0,0,0,.25);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;-ms-scroll-snap-type:y mandatory;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:1}.md-version:focus-within .md-version__list,.md-version:hover .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (pointer:coarse){.md-version:hover .md-version__list{-webkit-animation:hoverfix .25s forwards;animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{-webkit-animation:none;animation:none}}.md-version__item{line-height:1.8rem}.md-version__link{cursor:pointer;display:block;outline:none;padding-left:.6rem;padding-right:1.2rem;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link:focus,.md-version__link:hover{color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .admonition,.md-typeset details{background-color:var(--md-admonition-bg-color);border-left:.2rem solid #448aff;border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 .025rem .05rem rgba(0,0,0,.05);color:var(--md-admonition-fg-color);font-size:.64rem;margin:1.5625em 0;overflow:hidden;padding:0 .6rem;page-break-inside:avoid}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}[dir=rtl] .md-typeset .admonition,[dir=rtl] .md-typeset details{border-left:none;border-right:.2rem solid #448aff}.md-typeset .admonition .admonition,.md-typeset .admonition details,.md-typeset details .admonition,.md-typeset details details{margin-bottom:1em;margin-top:1em}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}.md-typeset .admonition-title,.md-typeset summary{background-color:rgba(68,138,255,.1);border-left:.2rem solid #448aff;font-weight:700;margin:0 -.6rem 0 -.8rem;padding:.4rem .6rem .4rem 2rem;position:relative}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-left:none;border-right:.2rem solid #448aff;margin:0 -.8rem 0 -.6rem;padding:.4rem 2rem .4rem .6rem}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;left:.6rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;width:1rem}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{left:auto;right:.6rem}.md-typeset .admonition-title+.tabbed-set:last-child,.md-typeset summary+.tabbed-set:last-child{margin-top:0}.md-typeset .admonition.note,.md-typeset details.note{border-color:#448aff}.md-typeset .note>.admonition-title,.md-typeset .note>summary{background-color:rgba(68,138,255,.1);border-color:#448aff}.md-typeset .note>.admonition-title:before,.md-typeset .note>summary:before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.abstract,.md-typeset .admonition.summary,.md-typeset .admonition.tldr,.md-typeset details.abstract,.md-typeset details.summary,.md-typeset details.tldr{border-color:#00b0ff}.md-typeset .abstract>.admonition-title,.md-typeset .abstract>summary,.md-typeset .summary>.admonition-title,.md-typeset .summary>summary,.md-typeset .tldr>.admonition-title,.md-typeset .tldr>summary{background-color:rgba(0,176,255,.1);border-color:#00b0ff}.md-typeset .abstract>.admonition-title:before,.md-typeset .abstract>summary:before,.md-typeset .summary>.admonition-title:before,.md-typeset .summary>summary:before,.md-typeset .tldr>.admonition-title:before,.md-typeset .tldr>summary:before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.info,.md-typeset .admonition.todo,.md-typeset details.info,.md-typeset details.todo{border-color:#00b8d4}.md-typeset .info>.admonition-title,.md-typeset .info>summary,.md-typeset .todo>.admonition-title,.md-typeset .todo>summary{background-color:rgba(0,184,212,.1);border-color:#00b8d4}.md-typeset .info>.admonition-title:before,.md-typeset .info>summary:before,.md-typeset .todo>.admonition-title:before,.md-typeset .todo>summary:before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.hint,.md-typeset .admonition.important,.md-typeset .admonition.tip,.md-typeset details.hint,.md-typeset details.important,.md-typeset details.tip{border-color:#00bfa5}.md-typeset .hint>.admonition-title,.md-typeset .hint>summary,.md-typeset .important>.admonition-title,.md-typeset .important>summary,.md-typeset .tip>.admonition-title,.md-typeset .tip>summary{background-color:rgba(0,191,165,.1);border-color:#00bfa5}.md-typeset .hint>.admonition-title:before,.md-typeset .hint>summary:before,.md-typeset .important>.admonition-title:before,.md-typeset .important>summary:before,.md-typeset .tip>.admonition-title:before,.md-typeset .tip>summary:before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.check,.md-typeset .admonition.done,.md-typeset .admonition.success,.md-typeset details.check,.md-typeset details.done,.md-typeset details.success{border-color:#00c853}.md-typeset .check>.admonition-title,.md-typeset .check>summary,.md-typeset .done>.admonition-title,.md-typeset .done>summary,.md-typeset .success>.admonition-title,.md-typeset .success>summary{background-color:rgba(0,200,83,.1);border-color:#00c853}.md-typeset .check>.admonition-title:before,.md-typeset .check>summary:before,.md-typeset .done>.admonition-title:before,.md-typeset .done>summary:before,.md-typeset .success>.admonition-title:before,.md-typeset .success>summary:before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.faq,.md-typeset .admonition.help,.md-typeset .admonition.question,.md-typeset details.faq,.md-typeset details.help,.md-typeset details.question{border-color:#64dd17}.md-typeset .faq>.admonition-title,.md-typeset .faq>summary,.md-typeset .help>.admonition-title,.md-typeset .help>summary,.md-typeset .question>.admonition-title,.md-typeset .question>summary{background-color:rgba(100,221,23,.1);border-color:#64dd17}.md-typeset .faq>.admonition-title:before,.md-typeset .faq>summary:before,.md-typeset .help>.admonition-title:before,.md-typeset .help>summary:before,.md-typeset .question>.admonition-title:before,.md-typeset .question>summary:before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.attention,.md-typeset .admonition.caution,.md-typeset .admonition.warning,.md-typeset details.attention,.md-typeset details.caution,.md-typeset details.warning{border-color:#ff9100}.md-typeset .attention>.admonition-title,.md-typeset .attention>summary,.md-typeset .caution>.admonition-title,.md-typeset .caution>summary,.md-typeset .warning>.admonition-title,.md-typeset .warning>summary{background-color:rgba(255,145,0,.1);border-color:#ff9100}.md-typeset .attention>.admonition-title:before,.md-typeset .attention>summary:before,.md-typeset .caution>.admonition-title:before,.md-typeset .caution>summary:before,.md-typeset .warning>.admonition-title:before,.md-typeset .warning>summary:before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.fail,.md-typeset .admonition.failure,.md-typeset .admonition.missing,.md-typeset details.fail,.md-typeset details.failure,.md-typeset details.missing{border-color:#ff5252}.md-typeset .fail>.admonition-title,.md-typeset .fail>summary,.md-typeset .failure>.admonition-title,.md-typeset .failure>summary,.md-typeset .missing>.admonition-title,.md-typeset .missing>summary{background-color:rgba(255,82,82,.1);border-color:#ff5252}.md-typeset .fail>.admonition-title:before,.md-typeset .fail>summary:before,.md-typeset .failure>.admonition-title:before,.md-typeset .failure>summary:before,.md-typeset .missing>.admonition-title:before,.md-typeset .missing>summary:before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.danger,.md-typeset .admonition.error,.md-typeset details.danger,.md-typeset details.error{border-color:#ff1744}.md-typeset .danger>.admonition-title,.md-typeset .danger>summary,.md-typeset .error>.admonition-title,.md-typeset .error>summary{background-color:rgba(255,23,68,.1);border-color:#ff1744}.md-typeset .danger>.admonition-title:before,.md-typeset .danger>summary:before,.md-typeset .error>.admonition-title:before,.md-typeset .error>summary:before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.bug,.md-typeset details.bug{border-color:#f50057}.md-typeset .bug>.admonition-title,.md-typeset .bug>summary{background-color:rgba(245,0,87,.1);border-color:#f50057}.md-typeset .bug>.admonition-title:before,.md-typeset .bug>summary:before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.example,.md-typeset details.example{border-color:#7c4dff}.md-typeset .example>.admonition-title,.md-typeset .example>summary{background-color:rgba(124,77,255,.1);border-color:#7c4dff}.md-typeset .example>.admonition-title:before,.md-typeset .example>summary:before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset .admonition.cite,.md-typeset .admonition.quote,.md-typeset details.cite,.md-typeset details.quote{border-color:#9e9e9e}.md-typeset .cite>.admonition-title,.md-typeset .cite>summary,.md-typeset .quote>.admonition-title,.md-typeset .quote>summary{background-color:hsla(0,0%,62%,.1);border-color:#9e9e9e}.md-typeset .cite>.admonition-title:before,.md-typeset .cite>summary:before,.md-typeset .quote>.admonition-title:before,.md-typeset .quote>summary:before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}.md-typeset .footnote>ol{margin-left:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:hover .footnote-backref,.md-typeset .footnote>ol>li:target .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateX(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateX(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentColor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before svg{transform:scaleX(-1)}.md-typeset [id^="fnref:"]:target{margin-top:-3.4rem;padding-top:3.4rem;scroll-margin-top:0}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset [id^="fn:"]:target{margin-top:-3.45rem;padding-top:3.45rem;scroll-margin-top:0}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;margin-left:.5rem;opacity:0;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}[dir=rtl] .md-typeset .headerlink{margin-left:0;margin-right:.5rem}.md-typeset .headerlink:focus,.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset .headerlink:hover,.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{scroll-margin-top:3.6rem}.md-typeset h1:target,.md-typeset h2:target,.md-typeset h3:target{scroll-margin-top:0}.md-typeset h1:target:before,.md-typeset h2:target:before,.md-typeset h3:target:before{content:"";display:block;margin-top:-3.4rem;padding-top:3.4rem}.md-typeset h4:target{scroll-margin-top:0}.md-typeset h4:target:before{content:"";display:block;margin-top:-3.45rem;padding-top:3.45rem}.md-typeset h5:target,.md-typeset h6:target{scroll-margin-top:0}.md-typeset h5:target:before,.md-typeset h6:target:before{content:"";display:block;margin-top:-3.6rem;padding-top:3.6rem}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.9375em){.md-typeset div.arithmatex{margin:0 -.8rem}}.md-typeset div.arithmatex>*{margin:1em auto!important;padding:0 .8rem;touch-action:auto;width:-webkit-min-content;width:-moz-min-content;width:min-content}.md-typeset .critic.comment,.md-typeset del.critic,.md-typeset ins.critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}.md-typeset details:after{content:"";display:table}.md-typeset summary{border-top-left-radius:.1rem;border-top-right-radius:.1rem;cursor:pointer;display:block;min-height:1rem;padding:.4rem 1.8rem .4rem 2rem}[dir=rtl] .md-typeset summary{padding:.4rem 2.2rem .4rem 1.8rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset summary:after{background-color:currentColor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;right:.4rem;top:.4rem;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{left:.4rem;right:auto;transform:rotate(180deg)}.md-typeset summary::-webkit-details-marker,.md-typeset summary::marker{display:none}.md-typeset .emojione,.md-typeset .gemoji,.md-typeset .twemoji{display:inline-flex;height:1.125em;vertical-align:text-top}.md-typeset .emojione svg,.md-typeset .gemoji svg,.md-typeset .twemoji svg{fill:currentColor;max-height:100%;width:1.125em}.highlight .o,.highlight .ow{color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight .cpf,.highlight .l,.highlight .s,.highlight .s1,.highlight .s2,.highlight .sb,.highlight .sc,.highlight .si,.highlight .ss{color:var(--md-code-hl-string-color)}.highlight .cp,.highlight .se,.highlight .sh,.highlight .sr,.highlight .sx{color:var(--md-code-hl-special-color)}.highlight .il,.highlight .m,.highlight .mb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:var(--md-code-hl-number-color)}.highlight .k,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr,.highlight .kt{color:var(--md-code-hl-keyword-color)}.highlight .kc,.highlight .n{color:var(--md-code-hl-name-color)}.highlight .bp,.highlight .nb,.highlight .no{color:var(--md-code-hl-constant-color)}.highlight .nc,.highlight .ne,.highlight .nf,.highlight .nn{color:var(--md-code-hl-function-color)}.highlight .nd,.highlight .ni,.highlight .nl,.highlight .nt{color:var(--md-code-hl-keyword-color)}.highlight .c,.highlight .c1,.highlight .ch,.highlight .cm,.highlight .cs,.highlight .sd{color:var(--md-code-hl-comment-color)}.highlight .na,.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi{color:var(--md-code-hl-variable-color)}.highlight .ge,.highlight .gh,.highlight .go,.highlight .gp,.highlight .gr,.highlight .gs,.highlight .gt,.highlight .gu{color:var(--md-code-hl-generic-color)}.highlight .gd,.highlight .gi{border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color);display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:-webkit-sticky;position:sticky;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.highlighttable{display:flow-root;overflow:hidden}.highlighttable tbody,.highlighttable td{display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;padding-right:.5882352941em}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .code{flex:1;overflow:hidden}.md-typeset .highlighttable{border-radius:.1rem;direction:ltr;margin:1em 0}.md-typeset .highlighttable code{border-radius:0}@media screen and (max-width:44.9375em){.md-typeset>.highlight{margin:1em -.8rem}.md-typeset>.highlight .hll{margin:0 -.8rem;padding:0 .8rem}.md-typeset>.highlight code{border-radius:0}.md-typeset>.highlighttable{border-radius:0;margin:1em -.8rem}.md-typeset>.highlighttable .hll{margin:0 -.8rem;padding:0 .8rem}}.md-typeset .keys kbd:after,.md-typeset .keys kbd:before{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-left-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-left-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-right-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-right-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-left-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-right-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}.md-typeset .tabbed-content{box-shadow:0 -.05rem var(--md-default-fg-color--lightest);display:none;order:99;width:100%}@media print{.md-typeset .tabbed-content{display:block;order:0}}.md-typeset .tabbed-content>.highlight:only-child pre,.md-typeset .tabbed-content>.highlighttable:only-child,.md-typeset .tabbed-content>pre:only-child{margin:0}.md-typeset .tabbed-content>.highlight:only-child pre>code,.md-typeset .tabbed-content>.highlighttable:only-child>code,.md-typeset .tabbed-content>pre:only-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-content>.tabbed-set{margin:0}.md-typeset .tabbed-set{border-radius:.1rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:checked+label{border-color:var(--md-accent-fg-color);color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:checked+label+.tabbed-content{display:block}.md-typeset .tabbed-set>input:focus+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-typeset .tabbed-set>input:not(.focus-visible)+label{-webkit-tap-highlight-color:transparent;outline:none}.md-typeset .tabbed-set>label{border-bottom:.1rem solid transparent;color:var(--md-default-fg-color--light);cursor:pointer;font-size:.64rem;font-weight:700;padding:.9375em 1.25em .78125em;transition:color .25s;width:auto;z-index:1}.md-typeset .tabbed-set>label:hover{color:var(--md-accent-fg-color)}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8,');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .task-list-item{list-style-type:none;position:relative}.md-typeset .task-list-item [type=checkbox]{left:-2em;position:absolute;top:.45em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{left:auto;right:-2em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lightest);content:"";height:1.25em;left:-1.5em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.15em;width:1.25em}[dir=rtl] .md-typeset .task-list-indicator:before{left:auto;right:-1.5em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}@media screen and (min-width:45em){.md-typeset .inline{float:left;margin-bottom:.8rem;margin-right:.8rem;margin-top:0;width:11.7rem}[dir=rtl] .md-typeset .inline{float:right;margin-left:.8rem;margin-right:0}.md-typeset .inline.end{float:right;margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{float:left;margin-left:0;margin-right:.8rem}} +/*# sourceMappingURL=main.875de78c.min.css.map */ \ No newline at end of file diff --git a/v10.0/assets/stylesheets/palette.f1a3b89f.min.css b/v10.0/assets/stylesheets/palette.f1a3b89f.min.css new file mode 100644 index 00000000..8bbfcfe6 --- /dev/null +++ b/v10.0/assets/stylesheets/palette.f1a3b89f.min.css @@ -0,0 +1,2 @@ +[data-md-color-accent=red]{--md-accent-fg-color:#ff1947;--md-accent-fg-color--transparent:rgba(255,25,71,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=pink]{--md-accent-fg-color:#f50056;--md-accent-fg-color--transparent:rgba(245,0,86,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=purple]{--md-accent-fg-color:#df41fb;--md-accent-fg-color--transparent:rgba(223,65,251,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=deep-purple]{--md-accent-fg-color:#7c4dff;--md-accent-fg-color--transparent:rgba(124,77,255,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=indigo]{--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:rgba(82,108,254,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=blue]{--md-accent-fg-color:#4287ff;--md-accent-fg-color--transparent:rgba(66,135,255,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=light-blue]{--md-accent-fg-color:#0091eb;--md-accent-fg-color--transparent:rgba(0,145,235,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=cyan]{--md-accent-fg-color:#00bad6;--md-accent-fg-color--transparent:rgba(0,186,214,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=teal]{--md-accent-fg-color:#00bda4;--md-accent-fg-color--transparent:rgba(0,189,164,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=green]{--md-accent-fg-color:#00c753;--md-accent-fg-color--transparent:rgba(0,199,83,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=light-green]{--md-accent-fg-color:#63de17;--md-accent-fg-color--transparent:rgba(99,222,23,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=lime]{--md-accent-fg-color:#b0eb00;--md-accent-fg-color--transparent:rgba(176,235,0,0.1);--md-accent-bg-color:rgba(0,0,0,0.87);--md-accent-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-accent=yellow]{--md-accent-fg-color:#ffd500;--md-accent-fg-color--transparent:rgba(255,213,0,0.1);--md-accent-bg-color:rgba(0,0,0,0.87);--md-accent-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-accent=amber]{--md-accent-fg-color:#fa0;--md-accent-fg-color--transparent:rgba(255,170,0,0.1);--md-accent-bg-color:rgba(0,0,0,0.87);--md-accent-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-accent=orange]{--md-accent-fg-color:#ff9100;--md-accent-fg-color--transparent:rgba(255,145,0,0.1);--md-accent-bg-color:rgba(0,0,0,0.87);--md-accent-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-accent=deep-orange]{--md-accent-fg-color:#ff6e42;--md-accent-fg-color--transparent:rgba(255,110,66,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=red]{--md-primary-fg-color:#ef5552;--md-primary-fg-color--light:#e57171;--md-primary-fg-color--dark:#e53734;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=pink]{--md-primary-fg-color:#e92063;--md-primary-fg-color--light:#ec417a;--md-primary-fg-color--dark:#c3185d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=purple]{--md-primary-fg-color:#ab47bd;--md-primary-fg-color--light:#bb69c9;--md-primary-fg-color--dark:#8c24a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=deep-purple]{--md-primary-fg-color:#7e56c2;--md-primary-fg-color--light:#9574cd;--md-primary-fg-color--dark:#673ab6;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=indigo]{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=blue]{--md-primary-fg-color:#2094f3;--md-primary-fg-color--light:#42a5f5;--md-primary-fg-color--dark:#1975d2;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=light-blue]{--md-primary-fg-color:#02a6f2;--md-primary-fg-color--light:#28b5f6;--md-primary-fg-color--dark:#0287cf;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=cyan]{--md-primary-fg-color:#00bdd6;--md-primary-fg-color--light:#25c5da;--md-primary-fg-color--dark:#0097a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=teal]{--md-primary-fg-color:#009485;--md-primary-fg-color--light:#26a699;--md-primary-fg-color--dark:#007a6c;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=green]{--md-primary-fg-color:#4cae4f;--md-primary-fg-color--light:#68bb6c;--md-primary-fg-color--dark:#398e3d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=light-green]{--md-primary-fg-color:#8bc34b;--md-primary-fg-color--light:#9ccc66;--md-primary-fg-color--dark:#689f38;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=lime]{--md-primary-fg-color:#cbdc38;--md-primary-fg-color--light:#d3e156;--md-primary-fg-color--dark:#b0b52c;--md-primary-bg-color:rgba(0,0,0,0.87);--md-primary-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-primary=yellow]{--md-primary-fg-color:#ffec3d;--md-primary-fg-color--light:#ffee57;--md-primary-fg-color--dark:#fbc02d;--md-primary-bg-color:rgba(0,0,0,0.87);--md-primary-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-primary=amber]{--md-primary-fg-color:#ffc105;--md-primary-fg-color--light:#ffc929;--md-primary-fg-color--dark:#ffa200;--md-primary-bg-color:rgba(0,0,0,0.87);--md-primary-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-primary=orange]{--md-primary-fg-color:#ffa724;--md-primary-fg-color--light:#ffa724;--md-primary-fg-color--dark:#fa8900;--md-primary-bg-color:rgba(0,0,0,0.87);--md-primary-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-primary=deep-orange]{--md-primary-fg-color:#ff6e42;--md-primary-fg-color--light:#ff8a66;--md-primary-fg-color--dark:#f4511f;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=brown]{--md-primary-fg-color:#795649;--md-primary-fg-color--light:#8d6e62;--md-primary-fg-color--dark:#5d4037;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=grey]{--md-primary-fg-color:#757575;--md-primary-fg-color--light:#9e9e9e;--md-primary-fg-color--dark:#616161;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=blue-grey]{--md-primary-fg-color:#546d78;--md-primary-fg-color--light:#607c8a;--md-primary-fg-color--dark:#455a63;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=white]{--md-primary-fg-color:#fff;--md-primary-fg-color--light:hsla(0,0%,100%,0.7);--md-primary-fg-color--dark:rgba(0,0,0,0.07);--md-primary-bg-color:rgba(0,0,0,0.87);--md-primary-bg-color--light:rgba(0,0,0,0.54);--md-typeset-a-color:#4051b5}@media screen and (min-width:60em){[data-md-color-primary=white] .md-search__input{background-color:rgba(0,0,0,.07)}[data-md-color-primary=white] .md-search__input+.md-search__icon{color:rgba(0,0,0,.87)}[data-md-color-primary=white] .md-search__input::-webkit-input-placeholder{color:rgba(0,0,0,.54)}[data-md-color-primary=white] .md-search__input::-moz-placeholder{color:rgba(0,0,0,.54)}[data-md-color-primary=white] .md-search__input::-ms-input-placeholder{color:rgba(0,0,0,.54)}[data-md-color-primary=white] .md-search__input::placeholder{color:rgba(0,0,0,.54)}[data-md-color-primary=white] .md-search__input:hover{background-color:rgba(0,0,0,.32)}}@media screen and (min-width:76.25em){[data-md-color-primary=white] .md-tabs{border-bottom:.05rem solid rgba(0,0,0,.07)}}[data-md-color-primary=black]{--md-primary-fg-color:#000;--md-primary-fg-color--light:rgba(0,0,0,0.54);--md-primary-fg-color--dark:#000;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7);--md-typeset-a-color:#4051b5}[data-md-color-primary=black] .md-header{background-color:#000}@media screen and (max-width:59.9375em){[data-md-color-primary=black] .md-nav__source{background-color:rgba(0,0,0,.87)}}@media screen and (min-width:60em){[data-md-color-primary=black] .md-search__input{background-color:hsla(0,0%,100%,.12)}[data-md-color-primary=black] .md-search__input:hover{background-color:hsla(0,0%,100%,.3)}}@media screen and (max-width:76.1875em){html [data-md-color-primary=black] .md-nav--primary .md-nav__title[for=__drawer]{background-color:#000}}@media screen and (min-width:76.25em){[data-md-color-primary=black] .md-tabs{background-color:#000}}@media screen{[data-md-color-scheme=slate]{--md-hue:232;--md-default-fg-color:hsla(var(--md-hue),75%,95%,1);--md-default-fg-color--light:hsla(var(--md-hue),75%,90%,0.62);--md-default-fg-color--lighter:hsla(var(--md-hue),75%,90%,0.32);--md-default-fg-color--lightest:hsla(var(--md-hue),75%,90%,0.12);--md-default-bg-color:hsla(var(--md-hue),15%,21%,1);--md-default-bg-color--light:hsla(var(--md-hue),15%,21%,0.54);--md-default-bg-color--lighter:hsla(var(--md-hue),15%,21%,0.26);--md-default-bg-color--lightest:hsla(var(--md-hue),15%,21%,0.07);--md-code-fg-color:hsla(var(--md-hue),18%,86%,1);--md-code-bg-color:hsla(var(--md-hue),15%,15%,1);--md-code-hl-color:rgba(66,135,255,0.15);--md-code-hl-number-color:#e6695b;--md-code-hl-special-color:#f06090;--md-code-hl-function-color:#c973d9;--md-code-hl-constant-color:#9383e2;--md-code-hl-keyword-color:#6791e0;--md-code-hl-string-color:#2fb170;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-mark-color:rgba(66,135,255,0.3);--md-typeset-kbd-color:hsla(var(--md-hue),15%,94%,0.12);--md-typeset-kbd-accent-color:hsla(var(--md-hue),15%,94%,0.2);--md-typeset-kbd-border-color:hsla(var(--md-hue),15%,14%,1);--md-admonition-bg-color:hsla(var(--md-hue),0%,100%,0.025);--md-footer-bg-color:hsla(var(--md-hue),15%,12%,0.87);--md-footer-bg-color--dark:hsla(var(--md-hue),15%,10%,1)}[data-md-color-scheme=slate][data-md-color-primary=black],[data-md-color-scheme=slate][data-md-color-primary=white]{--md-typeset-a-color:#5d6cc0}} +/*# sourceMappingURL=palette.f1a3b89f.min.css.map */ \ No newline at end of file diff --git a/v10.0/config/advanced/auth-ldap/index.html b/v10.0/config/advanced/auth-ldap/index.html new file mode 100644 index 00000000..532a4b58 --- /dev/null +++ b/v10.0/config/advanced/auth-ldap/index.html @@ -0,0 +1,1703 @@ + + + + + + + + + + + + + + + + + + + + + + + Advanced | LDAP Authentication - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + +
+
+ + + + + + + +

LDAP Authentication

+ +

Introduction

+

Getting started with ldap and this mailserver we need to take 3 parts in account:

+
    +
  • postfix for incoming & outgoing email
  • +
  • dovecot for accessing mailboxes
  • +
  • saslauthd for SMTP authentication (this can also be delegated to dovecot)
  • +
+

Variables to Control Provisioning by the Container

+

Have a look at the ENV page for information on the default values.

+

LDAP_QUERY_FILTER_*

+

Those variables contain the LDAP lookup filters for postfix, using %s as the placeholder for the domain or email address in question. This means that...

+
    +
  • ...for incoming email, the domain must return an entry for the DOMAIN filter (see virtual_alias_domains).
  • +
  • ...for incoming email, the inboxes which receive the email are chosen by the USER, ALIAS and GROUP filters.
      +
    • The USER filter specifies personal mailboxes, for which only one should exist per address, for example (mail=%s) (also see virtual_mailbox_maps)
    • +
    • The ALIAS filter specifies aliases for mailboxes, using virtual_alias_maps, for example (mailAlias=%s)
    • +
    • The GROUP filter specifies the personal mailboxes in a group (for emails that multiple people shall receive), using virtual_alias_maps, for example (mailGroupMember=%s)
    • +
    • Technically, there is no difference between ALIAS and GROUP, but ideally you should use ALIAS for personal aliases for a singular person (like ceo@example.org) and GROUP for multiple people (like hr@example.org).
    • +
    +
  • +
  • ...for outgoing email, the sender address is put through the SENDERS filter, and only if the authenticated user is one of the returned entries, the email can be sent.
      +
    • This only applies if SPOOF_PROTECTION=1.
    • +
    • If the SENDERS filter is missing, the USER, ALIAS and GROUP filters will be used in in a disjunction (OR).
    • +
    • To for example allow users from the admin group to spoof any sender email address, and to force everyone else to only use their personal mailbox address for outgoing email, you can use something like this: (|(memberOf=cn=admin,*)(mail=%s))
    • +
    +
  • +
+
Example

A really simple LDAP_QUERY_FILTER configuration, using only the user filter and allowing only admin@* to spoof any sender addresses.

+
- ENABLE_LDAP=1
+- LDAP_SERVER_HOST=ldap.example.org
+- LDAP_SEARCH_BASE=dc=example,dc=org"
+- LDAP_BIND_DN=cn=admin,dc=example,dc=org
+- LDAP_BIND_PW=mypassword
+- SPOOF_PROTECTION=1
+
+- LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s)
+- LDAP_QUERY_FILTER_USER=(mail=%s)
+- LDAP_QUERY_FILTER_ALIAS=(|) # doesn't match anything
+- LDAP_QUERY_FILTER_GROUP=(|) # doesn't match anything
+- LDAP_QUERY_FILTER_SENDERS=(|(mail=%s)(mail=admin@*))
+
+
+

DOVECOT_*_FILTER & DOVECOT_*_ATTRS

+

These variables specify the LDAP filters that dovecot uses to determine if a user can log in to their IMAP account, and which mailbox is responsible to receive email for a specific postfix user.

+

This is split into the following two lookups, both using %u as the placeholder for the full login name (see dovecot documentation for a full list of placeholders). Usually you only need to set DOVECOT_USER_FILTER, in which case it will be used for both filters.

+
    +
  • DOVECOT_USER_FILTER is used to get the account details (uid, gid, home directory, quota, ...) of a user.
  • +
  • DOVECOT_PASS_FILTER is used to get the password information of the user, and is in pretty much all cases identical to DOVECOT_USER_FILTER (which is the default behaviour if left away).
  • +
+

If your directory doesn't have the postfix-book schema installed, then you must change the internal attribute handling for dovecot. For this you have to change the pass_attr and the user_attr mapping, as shown in the example below:

+
- DOVECOT_PASS_ATTRS=<YOUR_USER_IDENTIFIER_ATTRIBUTE>=user,<YOUR_USER_PASSWORD_ATTRIBUTE>=password
+- DOVECOT_USER_ATTRS=<YOUR_USER_HOME_DIRECTORY_ATTRIBUTE>=home,<YOUR_USER_MAILSTORE_ATTRIBUTE>=mail,<YOUR_USER_MAIL_UID_ATTRIBUTE>=uid,<YOUR_USER_MAIL_GID_ATTRIBUTE>=gid
+
+
+

Note

+

For DOVECOT_*_ATTRS, you can replace ldapAttr=dovecotAttr with =dovecotAttr=%{ldap:ldapAttr} for more flexibility, like for example =home=/var/mail/%{ldap:uid} or just =uid=5000.

+

A list of dovecot attributes can be found in the dovecot documentation.

+
+
Defaults
- DOVECOT_USER_ATTRS=mailHomeDirectory=home,mailUidNumber=uid,mailGidNumber=gid,mailStorageDirectory=mail
+- DOVECOT_PASS_ATTRS=uniqueIdentifier=user,userPassword=password
+- DOVECOT_USER_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
+
+
+
Example

Setup for a directory that has the qmail-schema installed and uses uid:

+
- DOVECOT_PASS_ATTRS=uid=user,userPassword=password
+- DOVECOT_USER_ATTRS=homeDirectory=home,qmailUID=uid,qmailGID=gid,mailMessageStore=mail
+- DOVECOT_USER_FILTER=(&(objectClass=qmailUser)(uid=%u)(accountStatus=active))
+
+
+

The LDAP server configuration for dovecot will be taken mostly from postfix, other options can be found in the environment section in the docs.

+

DOVECOT_AUTH_BIND

+

Set this to yes to enable authentication binds (more details in the dovecot documentation). Currently, only DN lookup is supported without further changes to the configuration files, so this is only useful when you want to bind as a readonly user without the permission to read passwords.

+

SASLAUTHD_LDAP_FILTER

+

This filter is used for saslauthd, which is called by postfix when someone is authenticating through SMTP (assuming that SASLAUTHD_MECHANISMS=ldap is being used). Note that you'll need to set up the LDAP server for saslauthd seperately from postfix.

+

The filter variables are explained in detail in the LDAP_SASLAUTHD file, but unfortunately, this method doesn't really support domains right now - that means that %U is the only token that makes sense in this variable.

+
+

When to use this and how to avoid it

+

Using a separate filter for SMTP authentication allows you to for example allow noreply@example.org to send email, but not log in to IMAP or receive email: (&(mail=%U@example.org)(|(memberOf=cn=email,*)(mail=noreply@example.org)))

+

If you don't want to use a separate filter for SMTP authentication, you can set SASLAUTHD_MECHANISMS=rimap and SASLAUTHD_MECH_OPTIONS=127.0.0.1 to authenticate against dovecot instead - this means that the DOVECOT_USER_FILTER and DOVECOT_PASS_FILTER will be used for SMTP authentication as well.

+
+
Configure LDAP with saslauthd
- ENABLE_SASLAUTHD=1
+- SASLAUTHD_MECHANISMS=ldap
+- SASLAUTHD_LDAP_FILTER=(mail=%U@example.org)
+
+
+

Secure Connection with LDAPS or StartTLS

+

To enable LDAPS, all you need to do is to add the protocol to LDAP_SERVER_HOST, for example ldaps://example.org:636.

+

To enable LDAP over StartTLS (on port 389), you need to set the following environment variables instead (the protocol must not be ldaps:// in this case!):

+
- LDAP_START_TLS=yes
+- DOVECOT_TLS=yes
+- SASLAUTHD_LDAP_START_TLS=yes
+
+

LDAP Setup Examples

+
Basic Setup
version: '2'
+services:
+  mail:
+    image: mailserver/docker-mailserver:latest
+    hostname: mail
+    domainname: example.org
+    container_name: mail
+
+    ports:
+      - "25:25"
+      - "143:143"
+      - "587:587"
+      - "993:993"
+
+    volumes:
+      - maildata:/var/mail
+      - mailstate:/var/mail-state
+      - ./config/:/tmp/docker-mailserver/
+
+    environment:
+      - ENABLE_SPAMASSASSIN=1
+      - ENABLE_CLAMAV=1
+      - ENABLE_FAIL2BAN=1
+      - ENABLE_POSTGREY=1
+
+      # >>> Postfix LDAP Integration
+      - ENABLE_LDAP=1
+      - LDAP_SERVER_HOST=ldap.example.org
+      - LDAP_BIND_DN=cn=admin,ou=users,dc=example,dc=org
+      - LDAP_BIND_PW=mypassword
+      - LDAP_SEARCH_BASE=dc=example,dc=org
+      - LDAP_QUERY_FILTER_DOMAIN=(|(mail=*@%s)(mailAlias=*@%s)(mailGroupMember=*@%s))
+      - LDAP_QUERY_FILTER_USER=(&(objectClass=inetOrgPerson)(mail=%s))
+      - LDAP_QUERY_FILTER_ALIAS=(&(objectClass=inetOrgPerson)(mailAlias=%s))
+      - LDAP_QUERY_FILTER_GROUP=(&(objectClass=inetOrgPerson)(mailGroupMember=%s))
+      - LDAP_QUERY_FILTER_SENDERS=(&(objectClass=inetOrgPerson)(|(mail=%s)(mailAlias=%s)(mailGroupMember=%s)))
+      - SPOOF_PROTECTION=1
+      # <<< Postfix LDAP Integration
+
+      # >>> Dovecot LDAP Integration
+      - DOVECOT_USER_FILTER=(&(objectClass=inetOrgPerson)(mail=%u))
+      - DOVECOT_PASS_ATTRS=uid=user,userPassword=password
+      - DOVECOT_USER_ATTRS==home=/var/mail/%{ldap:uid},=mail=maildir:~/Maildir,uidNumber=uid,gidNumber=gid
+      # <<< Dovecot LDAP Integration
+
+      # >>> SASL LDAP Authentication
+      - ENABLE_SASLAUTHD=1
+      - SASLAUTHD_MECHANISMS=ldap
+      - SASLAUTHD_LDAP_FILTER=(&(mail=%U@example.org)(objectClass=inetOrgPerson))
+      # <<< SASL LDAP Authentication
+
+      - ONE_DIR=1
+      - DMS_DEBUG=0
+      - SSL_TYPE=letsencrypt
+      - PERMIT_DOCKER=host
+
+    cap_add:
+      - NET_ADMIN
+
+volumes:
+  maildata:
+    driver: local
+  mailstate:
+    driver: local
+
+
+
Kopano / Zarafa
version: '2'
+
+services:
+  mail:
+    image: mailserver/docker-mailserver:latest
+    hostname: mail
+    domainname: domain.com
+    container_name: mail
+
+    ports:
+      - "25:25"
+      - "143:143"
+      - "587:587"
+      - "993:993"
+
+    volumes:
+      - maildata:/var/mail
+      - mailstate:/var/mail-state
+      - ./config/:/tmp/docker-mailserver/
+
+    environment:
+      # We are not using dovecot here
+      - SMTP_ONLY=1
+      - ENABLE_SPAMASSASSIN=1
+      - ENABLE_CLAMAV=1
+      - ENABLE_FAIL2BAN=1
+      - ENABLE_POSTGREY=1
+      - SASLAUTHD_PASSWD=
+
+      # >>> SASL Authentication
+      - ENABLE_SASLAUTHD=1
+      - SASLAUTHD_LDAP_FILTER=(&(sAMAccountName=%U)(objectClass=person))
+      - SASLAUTHD_MECHANISMS=ldap
+      # <<< SASL Authentication
+
+      # >>> Postfix Ldap Integration
+      - ENABLE_LDAP=1
+      - LDAP_SERVER_HOST=<yourLdapContainer/yourLdapServer>
+      - LDAP_SEARCH_BASE=dc=mydomain,dc=loc
+      - LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=loc
+      - LDAP_BIND_PW=mypassword
+      - LDAP_QUERY_FILTER_USER=(&(objectClass=user)(mail=%s))
+      - LDAP_QUERY_FILTER_GROUP=(&(objectclass=group)(mail=%s))
+      - LDAP_QUERY_FILTER_ALIAS=(&(objectClass=user)(otherMailbox=%s))
+      - LDAP_QUERY_FILTER_DOMAIN=(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))
+      # <<< Postfix Ldap Integration
+
+      # >>> Kopano Integration
+      - ENABLE_POSTFIX_VIRTUAL_TRANSPORT=1
+      - POSTFIX_DAGENT=lmtp:kopano:2003
+      # <<< Kopano Integration
+
+      - ONE_DIR=1
+      - DMS_DEBUG=0
+      - SSL_TYPE=letsencrypt
+      - PERMIT_DOCKER=host
+
+    cap_add:
+      - NET_ADMIN
+
+volumes:
+  maildata:
+    driver: local
+  mailstate:
+    driver: local
+
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/full-text-search/index.html b/v10.0/config/advanced/full-text-search/index.html new file mode 100644 index 00000000..0f7f0f74 --- /dev/null +++ b/v10.0/config/advanced/full-text-search/index.html @@ -0,0 +1,1441 @@ + + + + + + + + + + + + + + + + + + + + + + + Advanced | Full-Text Search - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Full-Text Search

+ +

Overview

+

Full-text search allows all messages to be indexed, so that mail clients can quickly and efficiently search messages by their full text content.

+

The dovecot-solr Plugin is used in conjunction with Apache Solr running in a separate container. This is quite straightforward to setup using the following instructions.

+

Setup Steps

+
    +
  1. +

    docker-compose.yml:

    +
      solr:
    +    image: lmmdock/dovecot-solr:latest
    +    volumes:
    +      - solr-dovecot:/opt/solr/server/solr/dovecot
    +    restart: always
    +
    +  mailserver:
    +    depends_on:
    +      - solr
    +    image: mailserver/docker-mailserver:latest
    +    ...
    +    volumes:
    +      ...
    +      - ./etc/dovecot/conf.d/10-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro
    +    ...
    +
    +volumes:
    +  solr-dovecot:
    +    driver: local
    +
    +
  2. +
  3. +

    etc/dovecot/conf.d/10-plugin.conf:

    +
    mail_plugins = $mail_plugins fts fts_solr
    +
    +plugin {
    +  fts = solr
    +  fts_autoindex = yes
    +  fts_solr = url=http://solr:8983/solr/dovecot/ 
    +}
    +
    +
  4. +
  5. +

    Recreate containers: docker-compose down ; docker-compose up -d

    +
  6. +
  7. Flag all user mailbox FTS indexes as invalid, so they are rescanned on demand when they are next searched: docker-compose exec mailserver doveadm fts rescan -A
  8. +
+

Further Discussion

+

See #905

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/ipv6/index.html b/v10.0/config/advanced/ipv6/index.html new file mode 100644 index 00000000..82122ae0 --- /dev/null +++ b/v10.0/config/advanced/ipv6/index.html @@ -0,0 +1,1430 @@ + + + + + + + + + + + + + + + + + + + + + + + Advanced | IPv6 - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

IPv6

+ +

Background

+

If your container host supports IPv6, then docker-mailserver will automatically accept IPv6 connections by way of the docker host's IPv6. However, incoming mail will fail SPF checks because they will appear to come from the IPv4 gateway that docker is using to proxy the IPv6 connection (172.20.0.1 is the gateway).

+

This can be solved by supporting IPv6 connections all the way to the docker-mailserver container.

+

Setup steps

+
+++ b/serv/docker-compose.yml
+@@ -1,4 +1,4 @@
+-version: '2'
++version: '2.1'
+
+@@ -32,6 +32,16 @@ services:
+
++  ipv6nat:
++    image: robbertkl/ipv6nat
++    restart: always
++    network_mode: "host"
++    cap_add:
++      - NET_ADMIN
++      - SYS_MODULE
++    volumes:
++      - /var/run/docker.sock:/var/run/docker.sock:ro
++      - /lib/modules:/lib/modules:ro
+
+@@ -306,4 +316,13 @@ networks:
+
++  default:
++    driver: bridge
++    enable_ipv6: true
++    ipam:
++      driver: default
++      config:
++        - subnet: fd00:0123:4567::/48
++          gateway: fd00:0123:4567::1
+
+

Further Discussion

+

See #1438

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/kubernetes/index.html b/v10.0/config/advanced/kubernetes/index.html new file mode 100644 index 00000000..0f455ce9 --- /dev/null +++ b/v10.0/config/advanced/kubernetes/index.html @@ -0,0 +1,1982 @@ + + + + + + + + + + + + + + + + + + + + + + + Advanced | Kubernetes - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + +
+
+ + + + + + + +

Kubernetes

+ +

Deployment Example

+

There is nothing much in deploying mailserver to Kubernetes itself. The things are pretty same as in docker-compose.yml, but with Kubernetes syntax.

+
ConfigMap
apiVersion: v1
+kind: Namespace
+metadata:
+  name: mailserver
+---
+kind: ConfigMap
+apiVersion: v1
+metadata:
+  name: mailserver.env.config
+  namespace: mailserver
+  labels:
+    app: mailserver
+data:
+  OVERRIDE_HOSTNAME: example.com
+  ENABLE_FETCHMAIL: "0"
+  FETCHMAIL_POLL: "120"
+  ENABLE_SPAMASSASSIN: "0"
+  ENABLE_CLAMAV: "0"
+  ENABLE_FAIL2BAN: "0"
+  ENABLE_POSTGREY: "0"
+  ONE_DIR: "1"
+  DMS_DEBUG: "0"
+
+---
+kind: ConfigMap
+apiVersion: v1
+metadata:
+  name: mailserver.config
+  namespace: mailserver
+  labels:
+    app: mailserver
+data:
+  postfix-accounts.cf: |
+    user1@example.com|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1
+
+  postfix-virtual.cf: |
+    alias1@example.com user1@dexample.com
+
+  #dovecot.cf: |
+  #  service stats {
+  #    unix_listener stats-reader {
+  #      group = docker
+  #      mode = 0666
+  #    }
+  #    unix_listener stats-writer {
+  #      group = docker
+  #      mode = 0666
+  #    }
+  #  }
+
+  SigningTable: |
+    *@example.com mail._domainkey.example.com
+
+  KeyTable: |
+    mail._domainkey.example.com example.com:mail:/etc/opendkim/keys/example.com-mail.key
+
+  TrustedHosts: |
+    127.0.0.1
+    localhost
+
+  #user-patches.sh: |
+  #  #!/bin/bash
+
+  #fetchmail.cf: |
+
+
+
Secret
apiVersion: v1
+kind: Namespace
+metadata:
+  name: mailserver
+---
+kind: Secret
+apiVersion: v1
+metadata:
+  name: mailserver.opendkim.keys
+  namespace: mailserver
+  labels:
+    app: mailserver
+type: Opaque
+data:
+  example.com-mail.key: 'base64-encoded-DKIM-key'
+
+
+
Service
apiVersion: v1
+kind: Namespace
+metadata:
+  name: mailserver
+---
+kind: Service
+apiVersion: v1
+metadata:
+  name: mailserver
+  namespace: mailserver
+  labels:
+    app: mailserver
+spec:
+  selector:
+    app: mailserver
+  ports:
+    - name: smtp
+      port: 25
+      targetPort: smtp
+    - name: smtp-secure
+      port: 465
+      targetPort: smtp-secure
+    - name: smtp-auth
+      port: 587
+      targetPort: smtp-auth
+    - name: imap
+      port: 143
+      targetPort: imap
+    - name: imap-secure
+      port: 993
+      targetPort: imap-secure
+
+
+
Deployment
apiVersion: v1
+kind: Namespace
+metadata:
+  name: mailserver
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: mailserver
+  namespace: mailserver
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: mailserver
+  template:
+    metadata:
+      labels:
+        app: mailserver
+        role: mail
+        tier: backend
+    spec:
+      #nodeSelector:
+      #  kubernetes.io/hostname: local.k8s
+      #initContainers:
+      #- name: init-myservice
+      #  image: busybox
+      #  command: ["/bin/sh", "-c", "cp /tmp/user-patches.sh /tmp/files"]
+      #  volumeMounts:
+      #    - name: config
+      #      subPath: user-patches.sh
+      #      mountPath: /tmp/user-patches.sh
+      #      readOnly: true
+      #    - name: tmp-files
+      #      mountPath: /tmp/files
+      containers:
+      - name: docker-mailserver
+        image: mailserver/docker-mailserver:latest
+        imagePullPolicy: Always
+        securityContext:
+          capabilities:
+            # If Fail2Ban is not enabled, you can remove NET_ADMIN.
+            # If you are running on CRI-O, you will need the SYS_CHROOT capability,
+            # as it is no longer a default capability.
+            add: ["NET_ADMIN", "SYS_PTRACE", "SYS_CHROOT" ]
+        volumeMounts:
+          - name: config
+            subPath: postfix-accounts.cf
+            mountPath: /tmp/docker-mailserver/postfix-accounts.cf
+            readOnly: true
+          #- name: config
+          #  subPath: postfix-main.cf
+          #  mountPath: /tmp/docker-mailserver/postfix-main.cf
+          #  readOnly: true
+          - name: config
+            subPath: postfix-virtual.cf
+            mountPath: /tmp/docker-mailserver/postfix-virtual.cf
+            readOnly: true
+          - name: config
+            subPath: fetchmail.cf
+            mountPath: /tmp/docker-mailserver/fetchmail.cf
+            readOnly: true
+          - name: config
+            subPath: dovecot.cf
+            mountPath: /tmp/docker-mailserver/dovecot.cf
+            readOnly: true
+          #- name: config
+          #  subPath: user1.example.com.dovecot.sieve
+          #  mountPath: /tmp/docker-mailserver/user1@example.com.dovecot.sieve
+          #  readOnly: true
+          #- name: tmp-files
+          #  subPath: user-patches.sh
+          #  mountPath: /tmp/docker-mailserver/user-patches.sh
+          - name: config
+            subPath: SigningTable
+            mountPath: /tmp/docker-mailserver/opendkim/SigningTable
+            readOnly: true
+          - name: config
+            subPath: KeyTable
+            mountPath: /tmp/docker-mailserver/opendkim/KeyTable
+            readOnly: true
+          - name: config
+            subPath: TrustedHosts
+            mountPath: /tmp/docker-mailserver/opendkim/TrustedHosts
+            readOnly: true
+          - name: opendkim-keys
+            mountPath: /tmp/docker-mailserver/opendkim/keys
+            readOnly: true
+          - name: data
+            mountPath: /var/mail
+            subPath: data
+          - name: data
+            mountPath: /var/mail-state
+            subPath: state
+          - name: data
+            mountPath: /var/log/mail
+            subPath: log
+        ports:
+          - name: smtp
+            containerPort: 25
+            protocol: TCP
+          - name: smtp-secure
+            containerPort: 465
+            protocol: TCP
+          - name: smtp-auth
+            containerPort: 587
+          - name: imap
+            containerPort: 143
+            protocol: TCP
+          - name: imap-secure
+            containerPort: 993
+            protocol: TCP
+        envFrom:
+          - configMapRef:
+              name: mailserver.env.config
+      volumes:
+        - name: config
+          configMap:
+            name: mailserver.config
+        - name: opendkim-keys
+          secret:
+            secretName: mailserver.opendkim.keys
+        - name: data
+          persistentVolumeClaim:
+            claimName: mail-storage
+        - name: tmp-files
+          emptyDir: {}
+
+
+
+

Warning

+

Any sensitive data (keys, etc) should be deployed via Secrets. Other configuration just fits well into ConfigMaps.

+
+
+

Note

+

Make sure that Pod is assigned to specific Node in case you're using volume for data directly with hostPath. Otherwise Pod can be rescheduled on a different Node and previous data won't be found. Except the case when you're using some shared filesystem on your Nodes.

+
+
+

Note

+

If you experience issues with processes crashing showing an error like operation not permitted or postfix/pickup[987]: fatal: chroot(/var/spool/postfix): Operation not permitted, then you should add the SYS_CHROOT capability. Runtimes like CRI-O do not ship with this capability by default.

+
+

Exposing to the Outside World

+

The hard part with Kubernetes is to expose deployed mailserver to outside world. Kubernetes provides multiple ways for doing that. Each has its downsides and complexity.

+

The major problem with exposing mailserver to outside world in Kubernetes is to preserve real client IP. Real client IP is required by mailserver for performing IP-based SPF checks and spam checks.

+

Preserving real client IP is relatively non-trivial in Kubernetes and most exposing ways do not provide it. So, it's up to you to decide which exposing way suits better your needs in a price of complexity.

+

If you do not require SPF checks for incoming mails you may disable them in Postfix configuration by dropping following line (which removes check_policy_service unix:private/policyd-spf option):

+
+

Example

+
kind: ConfigMap
+apiVersion: v1
+metadata:
+  name: mailserver.config
+  labels:
+    app: mailserver
+data:
+  postfix-main.cf: |
+    smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net
+# ...
+
+---
+
+kind: Deployment
+apiVersion: extensions/v1beta1
+metadata:
+  name: mailserver
+# ...
+    volumeMounts:
+      - name: config
+        subPath: postfix-main.cf
+        mountPath: /tmp/docker-mailserver/postfix-main.cf
+        readOnly: true
+
+
+

External IPs Service

+

The simplest way is to expose mailserver as a Service with external IPs.

+
+

Example

+
kind: Service
+apiVersion: v1
+metadata:
+  name: mailserver
+  labels:
+    app: mailserver
+spec:
+  selector:
+    app: mailserver
+  ports:
+    - name: smtp
+      port: 25
+      targetPort: smtp
+# ...
+  externalIPs:
+    - 80.11.12.10
+
+
+

Downsides

+
    +
  • +

    Real client IP is not preserved, so SPF check of incoming mail will fail.

    +
  • +
  • +

    Requirement to specify exposed IPs explicitly.

    +
  • +
+

Proxy port to Service

+

The Proxy Pod helps to avoid necessity of specifying external IPs explicitly. This comes in price of complexity: you must deploy Proxy Pod on each Node you want to expose mailserver on.

+

Downsides

+
    +
  • Real client IP is not preserved, so SPF check of incoming mail will fail.
  • +
+

Bind to concrete Node and use host network

+

The simplest way to preserve real client IP is to use hostPort and hostNetwork: true in the mailserver Pod. This comes in price of availability: you can talk to mailserver from outside world only via IPs of Node where mailserver is deployed.

+
+

Example

+
kind: Deployment
+apiVersion: extensions/v1beta1
+metadata:
+  name: mailserver
+# ...
+    spec:
+      hostNetwork: true
+# ...
+      containers:
+# ...
+          ports:
+            - name: smtp
+              containerPort: 25
+              hostPort: 25
+            - name: smtp-auth
+              containerPort: 587
+              hostPort: 587
+            - name: imap-secure
+              containerPort: 993
+              hostPort: 993
+# ...
+
+
+

Downsides

+
    +
  • Not possible to access mailserver via other cluster Nodes, only via the one mailserver deployed at.
  • +
  • Every Port within the Container is exposed on the Host side, regardless of what the ports section in the Configuration defines.
  • +
+

Proxy Port to Service via PROXY Protocol

+

This way is ideologically the same as using Proxy Pod, but instead of a separate proxy pod, you configure your ingress to proxy TCP traffic to the mailserver pod using the PROXY protocol, which preserves the real client IP.

+

Configure your Ingress

+

With an NGINX ingress controller, set externalTrafficPolicy: Local for its service, and add the following to the TCP services config map (as described here):

+
25:  "mailserver/mailserver:25::PROXY"
+465: "mailserver/mailserver:465::PROXY"
+587: "mailserver/mailserver:587::PROXY"
+993: "mailserver/mailserver:993::PROXY"
+
+

With HAProxy, the configuration should look similar to the above. If you know what it actually looks like, add an example here. 😃

+

Configure the Mailserver

+

Then, configure both Postfix and Dovecot to expect the PROXY protocol:

+
+

Example

+
kind: ConfigMap
+apiVersion: v1
+metadata:
+  name: mailserver.config
+  labels:
+    app: mailserver
+data:
+  postfix-main.cf: |
+    postscreen_upstream_proxy_protocol = haproxy
+  postfix-master.cf: |
+    smtp/inet/postscreen_upstream_proxy_protocol=haproxy
+    submission/inet/smtpd_upstream_proxy_protocol=haproxy
+    smtps/inet/smtpd_upstream_proxy_protocol=haproxy
+  dovecot.cf: |
+    # Assuming your ingress controller is bound to 10.0.0.0/8
+    haproxy_trusted_networks = 10.0.0.0/8, 127.0.0.0/8
+    service imap-login {
+      inet_listener imap {
+        haproxy = yes
+      }
+      inet_listener imaps {
+        haproxy = yes
+      }
+    }
+# ...
+---
+
+kind: Deployment
+apiVersion: extensions/v1beta1
+metadata:
+  name: mailserver
+spec:
+  template:
+    spec:
+      containers:
+        - name: docker-mailserver
+          volumeMounts:
+            - name: config
+              subPath: postfix-main.cf
+              mountPath: /tmp/docker-mailserver/postfix-main.cf
+              readOnly: true
+            - name: config
+              subPath: postfix-master.cf
+              mountPath: /tmp/docker-mailserver/postfix-master.cf
+              readOnly: true
+            - name: config
+              subPath: dovecot.cf
+              mountPath: /tmp/docker-mailserver/dovecot.cf
+              readOnly: true
+
+
+

Downsides

+
    +
  • Not possible to access mailserver via inner cluster Kubernetes DNS, as PROXY protocol is required for incoming connections.
  • +
+

Let's Encrypt Certificates

+

Kube-Lego may be used for a role of Let's Encrypt client. It works with Kubernetes Ingress Resources and automatically issues/manages certificates/keys for exposed services via Ingresses.

+
+

Example

+
kind: Ingress
+apiVersion: extensions/v1beta1
+metadata:
+  name: mailserver
+  labels:
+    app: mailserver
+  annotations:
+    kubernetes.io/tls-acme: 'true'
+spec:
+  rules:
+    - host: example.com
+      http:
+        paths:
+          - path: /
+            backend:
+              serviceName: default-backend
+              servicePort: 80
+  tls:
+    - secretName: mailserver.tls
+      hosts:
+        - example.com
+
+
+

Now, you can use Let's Encrypt cert and key from mailserver.tls Secret in your Pod spec:

+
+

Example

+
# ...
+env:
+  - name: SSL_TYPE
+    value: 'manual'
+  - name: SSL_CERT_PATH
+    value: '/etc/ssl/mailserver/tls.crt'
+  - name: SSL_KEY_PATH
+    value: '/etc/ssl/mailserver/tls.key'
+# ...
+volumeMounts:
+  - name: tls
+    mountPath: /etc/ssl/mailserver
+    readOnly: true
+# ...
+volumes:
+  - name: tls
+    secret:
+      secretName: mailserver.tls
+
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/mail-fetchmail/index.html b/v10.0/config/advanced/mail-fetchmail/index.html new file mode 100644 index 00000000..3f6c61da --- /dev/null +++ b/v10.0/config/advanced/mail-fetchmail/index.html @@ -0,0 +1,1526 @@ + + + + + + + + + + + + + + + + + + + + + + + Advanced | Email Gathering with Fetchmail - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Email Gathering with Fetchmail

+ +

To enable the fetchmail service to retrieve e-mails set the environment variable ENABLE_FETCHMAIL to 1. Your docker-compose.yml file should look like following snippet:

+
environment:
+  - ENABLE_FETCHMAIL=1
+  - FETCHMAIL_POLL=300
+
+

Generate a file called fetchmail.cf and place it in the config folder. Your docker-mailserver folder should look like this example:

+
├── config
+│   ├── dovecot.cf
+│   ├── fetchmail.cf
+│   ├── postfix-accounts.cf
+│   └── postfix-virtual.cf
+├── docker-compose.yml
+└── README.md
+
+

Configuration

+

A detailed description of the configuration options can be found in the online version of the manual page.

+

IMAP Configuration

+
+

Example

+
poll 'imap.example.com' proto imap
+  user 'username'
+  pass 'secret'
+  is 'user1@domain.tld'
+  ssl
+
+
+

POP3 Configuration

+
+

Example

+
poll 'pop3.example.com' proto pop3
+  user 'username'
+  pass 'secret'
+  is 'user2@domain.tld'
+  ssl
+
+
+
+

Caution

+

Don’t forget the last line: eg: is 'user1@domain.tld'. After is you have to specify one email address from the configuration file config/postfix-accounts.cf.

+
+

More details how to configure fetchmail can be found in the fetchmail man page in the chapter “The run control file”.

+

Polling Interval

+

By default the fetchmail service searches every 5 minutes for new mails on your external mail accounts. You can override this default value by changing the ENV variable FETCHMAIL_POLL:

+
environment:
+  - FETCHMAIL_POLL=60
+
+

You must specify a numeric argument which is a polling interval in seconds. The example above polls every minute for new mails.

+

Debugging

+

To debug your fetchmail.cf configuration run this command:

+
./setup.sh debug fetchmail
+
+

For more informations about the configuration script setup.sh read the corresponding docs.

+

Here a sample output of ./setup.sh debug fetchmail:

+
fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:09 2016: poll started
+Trying to connect to 132.245.48.18/995...connected.
+fetchmail: Server certificate:
+fetchmail: Issuer Organization: Microsoft Corporation
+fetchmail: Issuer CommonName: Microsoft IT SSL SHA2
+fetchmail: Subject CommonName: outlook.com
+fetchmail: Subject Alternative Name: outlook.com
+fetchmail: Subject Alternative Name: *.outlook.com
+fetchmail: Subject Alternative Name: office365.com
+fetchmail: Subject Alternative Name: *.office365.com
+fetchmail: Subject Alternative Name: *.live.com
+fetchmail: Subject Alternative Name: *.internal.outlook.com
+fetchmail: Subject Alternative Name: *.outlook.office365.com
+fetchmail: Subject Alternative Name: outlook.office.com
+fetchmail: Subject Alternative Name: attachment.outlook.office.net
+fetchmail: Subject Alternative Name: attachment.outlook.officeppe.net
+fetchmail: Subject Alternative Name: *.office.com
+fetchmail: outlook.office365.com key fingerprint: 3A:A4:58:42:56:CD:BD:11:19:5B:CF:1E:85:16:8E:4D
+fetchmail: POP3< +OK The Microsoft Exchange POP3 service is ready. [SABFADEAUABSADAAMQBDAEEAMAAwADAANwAuAGUAdQByAHAAcgBkADAAMQAuAHAAcgBvAGQALgBlAHgAYwBoAGEAbgBnAGUAbABhAGIAcwAuAGMAbwBtAA==]
+fetchmail: POP3> CAPA
+fetchmail: POP3< +OK
+fetchmail: POP3< TOP
+fetchmail: POP3< UIDL
+fetchmail: POP3< SASL PLAIN
+fetchmail: POP3< USER
+fetchmail: POP3< .
+fetchmail: POP3> USER user1@outlook.com
+fetchmail: POP3< +OK
+fetchmail: POP3> PASS *
+fetchmail: POP3< +OK User successfully logged on.
+fetchmail: POP3> STAT
+fetchmail: POP3< +OK 0 0
+fetchmail: No mail for user1@outlook.com at outlook.office365.com
+fetchmail: POP3> QUIT
+fetchmail: POP3< +OK Microsoft Exchange Server 2016 POP3 server signing off.
+fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:11 2016: poll completed
+fetchmail: normal termination, status 1
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/mail-forwarding/aws-ses/index.html b/v10.0/config/advanced/mail-forwarding/aws-ses/index.html new file mode 100644 index 00000000..cd957b57 --- /dev/null +++ b/v10.0/config/advanced/mail-forwarding/aws-ses/index.html @@ -0,0 +1,1326 @@ + + + + + + + + + + + + + + + + + + + + + + + Mail Forwarding | AWS SES - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+ + + + + + + +

AWS SES

+ +
+

Warning

+

New configuration, see Configure Relay Hosts

+
+

Instead of letting postfix deliver mail directly it is possible to configure it to deliver outgoing email via Amazon SES (Simple Email Service). (Receiving inbound email via SES is not implemented.) The configuration follows the guidelines provided by AWS in https://docs.aws.amazon.com/ses/latest/DeveloperGuide/postfix.html, specifically, the STARTTLS method.

+

As described in the AWS Developer Guide you will have to generate SMTP credentials and define the following two environment variables in the docker-compose.yml with the appropriate values for your AWS SES subscription (the values for AWS_SES_USERPASS are the "SMTP username" and "SMTP password" provided when you create SMTP credentials for SES):

+
environment:
+  - AWS_SES_HOST=email-smtp.us-east-1.amazonaws.com
+  - AWS_SES_USERPASS=AKIAXXXXXXXXXXXXXXXX:kqXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+

If necessary, you can also provide AWS_SES_PORT. If not provided, it defaults to 25.

+

When you start the container you will see a log line as follows confirming the configuration:

+
Setting up outgoing email via AWS SES host email-smtp.us-east-1.amazonaws.com
+
+

To verify proper operation, send an email to some external account of yours and inspect the mail headers. You will also see the connection to SES in the mail logs. For example:

+
May 23 07:09:36 mail postfix/smtp[692]: Trusted TLS connection established to email-smtp.us-east-1.amazonaws.com[107.20.142.169]:25:
+TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)
+May 23 07:09:36 mail postfix/smtp[692]: 8C82A7E7: to=<someone@example.com>, relay=email-smtp.us-east-1.amazonaws.com[107.20.142.169]:25,
+delay=0.35, delays=0/0.02/0.13/0.2, dsn=2.0.0, status=sent (250 Ok 01000154dc729264-93fdd7ea-f039-43d6-91ed-653e8547867c-000000)
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/mail-forwarding/relay-hosts/index.html b/v10.0/config/advanced/mail-forwarding/relay-hosts/index.html new file mode 100644 index 00000000..88ad6615 --- /dev/null +++ b/v10.0/config/advanced/mail-forwarding/relay-hosts/index.html @@ -0,0 +1,1532 @@ + + + + + + + + + + + + + + + + + + + + + + + Mail Forwarding | Relay Hosts - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Relay Hosts

+ +

Introduction

+

Rather than having Postfix deliver mail directly, you can configure Postfix to send mail via another mail relay (smarthost). Examples include Mailgun, Sendgrid and AWS SES.

+

Depending on the domain of the sender, you may want to send via a different relay, or authenticate in a different way.

+

Basic Configuration

+

Basic configuration is done via environment variables:

+
    +
  • RELAY_HOST: default host to relay mail through, empty will disable this feature
  • +
  • RELAY_PORT: port on default relay, defaults to port 25
  • +
  • RELAY_USER: username for the default relay
  • +
  • RELAY_PASSWORD: password for the default user
  • +
+

Setting these environment variables will cause mail for all sender domains to be routed via the specified host, authenticating with the user/password combination.

+
+

Warning

+

For users of the previous AWS_SES_* variables: please update your configuration to use these new variables, no other configuration is required.

+
+

Advanced Configuration

+

Sender-dependent Authentication

+

Sender dependent authentication is done in config/postfix-sasl-password.cf. You can create this file manually, or use:

+
setup.sh relay add-auth <domain> <username> [<password>]
+
+

An example configuration file looks like this:

+
@domain1.com           relay_user_1:password_1
+@domain2.com           relay_user_2:password_2
+
+

If there is no other configuration, this will cause Postfix to deliver email throught the relay specified in RELAY_HOST env variable, authenticating as relay_user_1 when sent from domain1.com and authenticating as relay_user_2 when sending from domain2.com.

+
+

Note

+

To activate the configuration you must either restart the container, or you can also trigger an update by modifying a mail account.

+
+

Sender-dependent Relay Host

+

Sender dependent relay hosts are configured in config/postfix-relaymap.cf. You can create this file manually, or use:

+
setup.sh relay add-domain <domain> <host> [<port>]
+
+

An example configuration file looks like this:

+
@domain1.com        [relay1.org]:587
+@domain2.com        [relay2.org]:2525
+
+

Combined with the previous configuration in config/postfix-sasl-password.cf, this will cause Postfix to deliver mail sent from domain1.com via relay1.org:587, authenticating as relay_user_1, and mail sent from domain2.com via relay2.org:2525 authenticating as relay_user_2.

+
+

Note

+

You still have to define RELAY_HOST to activate the feature

+
+

Excluding Sender Domains

+

If you want mail sent from some domains to be delivered directly, you can exclude them from being delivered via the default relay by adding them to config/postfix-relaymap.cf with no destination. You can also do this via:

+
setup.sh relay exclude-domain <domain>
+
+

Extending the configuration file from above:

+
@domain1.com        [relay1.org]:587
+@domain2.com        [relay2.org]:2525
+@domain3.com
+
+

This will cause email sent from domain3.com to be delivered directly.

+

References

+

Thanks to the author of this article for the inspiration. This is also worth reading to understand a bit more about how to set up Mailgun to work with this.

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/mail-sieve/index.html b/v10.0/config/advanced/mail-sieve/index.html new file mode 100644 index 00000000..9d74cde2 --- /dev/null +++ b/v10.0/config/advanced/mail-sieve/index.html @@ -0,0 +1,1448 @@ + + + + + + + + + + + + + + + + + + + + + + + Advanced | Email Filtering with Sieve - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Email Filtering with Sieve

+ +

User-Defined Sieve Filters

+

Sieve allows to specify filtering rules for incoming emails that allow for example sorting mails into different folders depending on the title of an email. +There are global and user specific filters which are filtering the incoming emails in the following order:

+
    +
  • Global-before -> User specific -> Global-after
  • +
+

Global filters are applied to EVERY incoming mail for EVERY email address. +To specify a global Sieve filter provide a config/before.dovecot.sieve or a config/after.dovecot.sieve file with your filter rules. +If any filter in this filtering chain discards an incoming mail, the delivery process will stop as well and the mail will not reach any following filters(e.g. global-before stops an incoming spam mail: The mail will get discarded and a user-specific filter won't get applied.)

+

To specify a user-defined Sieve filter place a .dovecot.sieve file into a virtual user's mail folder e.g. /var/mail/domain.com/user1/.dovecot.sieve. If this file exists dovecot will apply the filtering rules.

+

It's even possible to install a user provided Sieve filter at startup during users setup: simply include a Sieve file in the config path for each user login that need a filter. The file name provided should be in the form <user_login>.dovecot.sieve, so for example for user1@domain.tld you should provide a Sieve file named config/user1@domain.tld.dovecot.sieve.

+

An example of a sieve filter that moves mails to a folder INBOX/spam depending on the sender address:

+
+

Example

+
require ["fileinto", "reject"];
+
+if address :contains ["From"] "spam@spam.com" {
+  fileinto "INBOX.spam";
+} else {
+  keep;
+}
+
+
+
+

Warning

+

That folders have to exist beforehand if sieve should move them.

+
+

Another example of a sieve filter that forward mails to a different address:

+
+

Example

+
require ["copy"];
+
+redirect :copy "user2@otherdomain.tld";
+
+
+

Just forward all incoming emails and do not save them locally:

+
+

Example

+
redirect "user2@otherdomain.tld";
+
+
+

You can also use external programs to filter or pipe (process) messages by adding executable scripts in config/sieve-pipe or config/sieve-filter. This can be used in lieu of a local alias file, for instance to forward an email to a webservice. These programs can then be referenced by filename, by all users. Note that the process running the scripts run as a privileged user. For further information see Dovecot's wiki.

+
require ["vnd.dovecot.pipe"];
+pipe "external-program";
+
+

For more examples or a detailed description of the Sieve language have a look at the official site. Other resources are available on the internet where you can find several examples.

+

Manage Sieve

+

The Manage Sieve extension allows users to modify their Sieve script by themselves. The authentication mechanisms are the same as for the main dovecot service. ManageSieve runs on port 4190 and needs to be enabled using the ENABLE_MANAGESIEVE=1 environment variable.

+
+

Example

+
# docker-compose.yml
+ports:
+  - "4190:4190"
+environment:
+  - ENABLE_MANAGESIEVE=1
+
+
+

All user defined sieve scripts that are managed by ManageSieve are stored in the user's home folder in /var/mail/domain.com/user1/sieve. Just one sieve script might be active for a user and is sym-linked to /var/mail/domain.com/user1/.dovecot.sieve automatically.

+
+

Note

+

ManageSieve makes sure to not overwrite an existing .dovecot.sieve file. If a user activates a new sieve script the old one is backuped and moved to the sieve folder.

+
+

The extension is known to work with the following ManageSieve clients:

+
    +
  • Sieve Editor a portable standalone application based on the former Thunderbird plugin.
  • +
  • Kmail the mail client of KDE's Kontact Suite.
  • +
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/maintenance/update-and-cleanup/index.html b/v10.0/config/advanced/maintenance/update-and-cleanup/index.html new file mode 100644 index 00000000..fef33f9c --- /dev/null +++ b/v10.0/config/advanced/maintenance/update-and-cleanup/index.html @@ -0,0 +1,1407 @@ + + + + + + + + + + + + + + + + + + + + + + + Maintenance | Update and Cleanup - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Update and Cleanup

+ +

Automatic Update

+

Docker images are handy but it can get a a hassle to keep them updated. Also when a repository is automated you want to get these images when they get out.

+

One could setup a complex action/hook-based workflow using probes, but there is a nice, easy to use docker image that solves this issue and could prove useful: watchtower.

+

A docker-compose example:

+
services:
+  watchtower:
+    restart: always
+    image: containrrr/watchtower:latest
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
+
+

For more details, see the manual

+

Automatic Cleanup

+

When you are pulling new images in automatically, it would be nice to have them cleaned up as well. There is also a docker image for this: spotify/docker-gc.

+

A docker-compose example:

+
services:
+  docker-gc:
+    restart: always
+    image: spotify/docker-gc:latest
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
+
+

For more details, see the manual

+

Or you can just use the --cleanup option provided by containrrr/watchtower.

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/optional-config/index.html b/v10.0/config/advanced/optional-config/index.html new file mode 100644 index 00000000..08a9d5a5 --- /dev/null +++ b/v10.0/config/advanced/optional-config/index.html @@ -0,0 +1,1420 @@ + + + + + + + + + + + + + + + + + + + + + + + Advanced | Optional Configuration - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + + + +
+
+
+ + + + + + +
+
+
+ + + + + + + + +
+
+ + + + + + + +

Optional Configuration

+ +

This is a list of all configuration files and directories which are optional or automatically generated in your config directory.

+

Directories

+
    +
  • sieve-filter: directory for sieve filter scripts. (Docs: Sieve)
  • +
  • sieve-pipe: directory for sieve pipe scripts. (Docs: Sieve)
  • +
  • opendkim: DKIM directory. Auto-configurable via setup.sh config dkim. (Docs: DKIM)
  • +
  • ssl: SSL Certificate directory. (Docs: SSL)
  • +
+

Files

+
    +
  • {user_email_address}.dovecot.sieve: User specific Sieve filter file. (Docs: Sieve)
  • +
  • before.dovecot.sieve: Global Sieve filter file, applied prior to the ${login}.dovecot.sieve filter. (Docs: Sieve)
  • +
  • after.dovecot.sieve: Global Sieve filter file, applied after the ${login}.dovecot.sieve filter. (Docs: Sieve)
  • +
  • postfix-main.cf: Every line will be added to the postfix main configuration. (Docs: Override Postfix Defaults)
  • +
  • postfix-master.cf: Every line will be added to the postfix master configuration. (Docs: Override Postfix Defaults)
  • +
  • postfix-accounts.cf: User accounts file. Modify via the setup.sh email script.
  • +
  • postfix-send-access.cf: List of users denied sending. Modify via setup.sh email restrict.
  • +
  • postfix-receive-access.cf: List of users denied receiving. Modify via setup.sh email restrict.
  • +
  • postfix-virtual.cf: Alias configuration file. Modify via setup.sh alias.
  • +
  • postfix-sasl-password.cf: listing of relayed domains with their respective <username>:<password>. Modify via setup.sh relay add-auth <domain> <username> [<password>]. (Docs: Relay-Hosts Auth)
  • +
  • postfix-relaymap.cf: domain-specific relays and exclusions. Modify via setup.sh relay add-domain and setup.sh relay exclude-domain. (Docs: Relay-Hosts Senders)
  • +
  • postfix-regexp.cf: Regular expression alias file. (Docs: Aliases)
  • +
  • ldap-users.cf: Configuration for the virtual user mapping virtual_mailbox_maps. See the setup-stack.sh script.
  • +
  • ldap-groups.cf: Configuration for the virtual alias mapping virtual_alias_maps. See the setup-stack.sh script.
  • +
  • ldap-aliases.cf: Configuration for the virtual alias mapping virtual_alias_maps. See the setup-stack.sh script.
  • +
  • ldap-domains.cf: Configuration for the virtual domain mapping virtual_mailbox_domains. See the setup-stack.sh script.
  • +
  • whitelist_clients.local: Whitelisted domains, not considered by postgrey. Enter one host or domain per line.
  • +
  • spamassassin-rules.cf: Antispam rules for Spamassassin. (Docs: FAQ - SpamAssassin Rules)
  • +
  • fail2ban-fail2ban.cf: Additional config options for fail2ban.cf. (Docs: Fail2Ban)
  • +
  • fail2ban-jail.cf: Additional config options for fail2ban's jail behaviour. (Docs: Fail2Ban)
  • +
  • amavis.cf: replaces the /etc/amavis/conf.d/50-user file
  • +
  • dovecot.cf: replaces /etc/dovecot/local.conf. (Docs: Override Dovecot Defaults)
  • +
  • dovecot-quotas.cf: list of custom quotas per mailbox. (Docs: Accounts)
  • +
  • user-patches.sh: this file will be run after all configuration files are set up, but before the postfix, amavis and other daemons are started. (Docs: FAQ - How to adjust settings with the user-patches.sh script)
  • +
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/override-defaults/dovecot/index.html b/v10.0/config/advanced/override-defaults/dovecot/index.html new file mode 100644 index 00000000..7eb6c457 --- /dev/null +++ b/v10.0/config/advanced/override-defaults/dovecot/index.html @@ -0,0 +1,1435 @@ + + + + + + + + + + + + + + + + + + + + + + + Override the Default Configs | Dovecot - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Dovecot

+ +

Add Configuration

+

The Dovecot default configuration can easily be extended providing a config/dovecot.cf file. +Dovecot documentation remains the best place to find configuration options.

+

Your docker-mailserver folder should look like this example:

+
├── config
+│   ├── dovecot.cf
+│   ├── postfix-accounts.cf
+│   └── postfix-virtual.cf
+├── docker-compose.yml
+└── README.md
+
+

One common option to change is the maximum number of connections per user:

+
mail_max_userip_connections = 100
+
+

Another important option is the default_process_limit (defaults to 100). If high-security mode is enabled you'll need to make sure this count is higher than the maximum number of users that can be logged in simultaneously.

+

This limit is quickly reached if users connect to the mail server with multiple end devices.

+

Override Configuration

+

For major configuration changes it’s best to override the dovecot configuration files. For each configuration file you want to override, add a list entry under the volumes key.

+

You will need to first obtain the configuration from the running container: mkdir -p ./config/dovecot && docker cp mailserver:/etc/dovecot/conf.d/10-master.conf ./config/dovecot/10-master.conf

+
services:
+  mail:
+    volumes:
+      - maildata:/var/mail
+      - ./config/dovecot/10-master.conf:/etc/dovecot/conf.d/10-master.conf
+
+

Debugging

+

To debug your dovecot configuration you can use:

+
    +
  • This command: ./setup.sh debug login doveconf | grep <some-keyword>
  • +
  • Or: docker exec -it mailserver doveconf | grep <some-keyword>
  • +
+
+

Note

+

setup.sh is included in the docker-mailserver repository. Make sure to grap the one matching your image version.

+
+

The config/dovecot.cf is copied internally to /etc/dovecot/local.conf. To check this file run:

+
docker exec -it mailserver cat /etc/dovecot/local.conf
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/override-defaults/postfix/index.html b/v10.0/config/advanced/override-defaults/postfix/index.html new file mode 100644 index 00000000..07b97bad --- /dev/null +++ b/v10.0/config/advanced/override-defaults/postfix/index.html @@ -0,0 +1,1327 @@ + + + + + + + + + + + + + + + + + + + + + + + Override the Default Configs | Postfix - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+ + + + + + + +

Postfix

+ +

The Postfix default configuration can easily be extended by providing a config/postfix-main.cf in postfix format. +This can also be used to add configuration that is not in our default configuration.

+

For example, one common use of this file is for increasing the default maximum message size:

+
# increase maximum message size
+message_size_limit = 52428800
+
+

That specific example is now supported and can be handled by setting POSTFIX_MESSAGE_SIZE_LIMIT.

+
+

Note

+

Postfix documentation remains the best place to find configuration options.

+
+

Each line in the provided file will be loaded into postfix.

+

In the same way it is possible to add a custom config/postfix-master.cf file that will override the standard master.cf. Each line in the file will be passed to postconf -P. The expected format is <service_name>/<type>/<parameter>, for example:

+
submission/inet/smtpd_reject_unlisted_recipient=no
+
+

Run postconf -P in the container without arguments to see the active master options.

+
+

Note

+

There should be no space between the parameter and the value.

+
+

Have a look at the code for more information.

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/advanced/override-defaults/user-patches/index.html b/v10.0/config/advanced/override-defaults/user-patches/index.html new file mode 100644 index 00000000..22a36bb6 --- /dev/null +++ b/v10.0/config/advanced/override-defaults/user-patches/index.html @@ -0,0 +1,1337 @@ + + + + + + + + + + + + + + + + + + + + + + + Custom User Changes & Patches | Scripting - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+ + + + + + + +

Modifications via Script

+ +

If you'd like to change, patch or alter files or behavior of docker-mailserver, you can use a script.

+

In case you cloned this repository, you can copy the file user-patches.sh.dist under config/ with cp config/user-patches.sh.dist config/user-patches.sh in order to create the user-patches.sh script. In case you are managing your directory structure yourself, create a config/ directory and the user-patches.sh file yourself.

+
# 1. Either create the config/ directory yourself
+#    or let docker-mailserver create it on initial
+#    startup
+~/somewhere $ mkdir config && cd config
+
+# 2. Create the user-patches.sh and edit it
+~/somewhere/config $ touch user-patches.sh
+~/somewhere/config $ vi user-patches.sh
+
+

The contents could look like this

+
#! /bin/bash
+
+cat >/etc/amavis/conf.d/50-user << "END"
+use strict;
+
+$undecipherable_subject_tag = undef;
+$admin_maps_by_ccat{+CC_UNCHECKED} =  undef;
+
+#------------ Do not modify anything below this line -------------
+1;  # ensure a defined return
+END
+
+...
+
+

And you're done. The user patches script runs right before starting daemons. That means, all the other configuration is in place, so the script can make final adjustments.

+
+

Note

+

Many "patches" can already be done with the Docker Compose-/Stack-file. Adding hostnames to /etc/hosts is done with the extra_hosts: section, sysctl commands can be managed with the sysctls: section, etc.

+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/best-practices/autodiscover/index.html b/v10.0/config/best-practices/autodiscover/index.html new file mode 100644 index 00000000..d2573d92 --- /dev/null +++ b/v10.0/config/best-practices/autodiscover/index.html @@ -0,0 +1,1309 @@ + + + + + + + + + + + + + + + + + + + + + + + Best Practices | Auto-discovery - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+ + + + + + + +

Auto-discovery

+ +

Email auto-discovery means a client email is able to automagically find out about what ports and security options to use, based on the mail server URL. It can help simplify the tedious / confusing task of adding own's email account for non-tech savvy users.

+

Email clients will search for auto-discoverable settings and prefill almost everything when a user enters its email address ❤

+

There exists autodiscover-email-settings on which provides IMAP/POP/SMTP/LDAP autodiscover capabilities on Microsoft Outlook/Apple Mail, autoconfig capabilities for Thunderbird or kmail and configuration profiles for iOS/Apple Mail.

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/best-practices/dkim/index.html b/v10.0/config/best-practices/dkim/index.html new file mode 100644 index 00000000..bceef57b --- /dev/null +++ b/v10.0/config/best-practices/dkim/index.html @@ -0,0 +1,1513 @@ + + + + + + + + + + + + + + + + + + + + + + + Best Practices | DKIM - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

DKIM

+ +

DKIM is a security measure targeting email spoofing. It is greatly recommended one activates it.

+
+

Note

+

See the Wikipedia page for more details on DKIM.

+
+

Enabling DKIM Signature

+

To enable DKIM signature, you must have created at least one email account. Once its done, just run the following command to generate the signature:

+
./setup.sh config dkim
+
+

After generating DKIM keys, you should restart the mail server. DNS edits may take a few minutes to hours to propagate. The script assumes you're being in the directory where the config/ directory is located. The default keysize when generating the signature is 4096 bits for now. If you need to change it (e.g. your DNS provider limits the size), then provide the size as the first parameter of the command:

+
./setup.sh config dkim keysize <keysize>
+
+

For LDAP systems that do not have any directly created user account you can run the following command (since 8.0.0) to generate the signature by additionally providing the desired domain name (if you have multiple domains use the command multiple times or provide a comma-separated list of domains):

+
./setup.sh config dkim keysize <key-size> domain <domain.tld>[,<domain2.tld>]
+
+

Now the keys are generated, you can configure your DNS server with DKIM signature, simply by adding a TXT record. If you have direct access to your DNS zone file, then it's only a matter of pasting the content of config/opendkim/keys/domain.tld/mail.txt in your domain.tld.hosts zone.

+
$ dig mail._domainkey.domain.tld TXT
+---
+;; ANSWER SECTION
+mail._domainkey.<DOMAIN> 300 IN TXT    "v=DKIM1; k=rsa; p=AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN"
+
+

Configuration using a Web Interface

+
    +
  1. Generate a new record of the type TXT.
  2. +
  3. Paste mail._domainkey the Name txt field.
  4. +
  5. In the Target or Value field fill in v=DKIM1; k=rsa; p=AZERTYUGHJKLMWX....
  6. +
  7. In TTL (time to live): Time span in seconds. How long the DNS server should cache the TXT record.
  8. +
  9. Save.
  10. +
+
+

Note

+

Sometimes the key in config/opendkim/keys/domain.tld/mail.txt can be on multiple lines. If so then you need to concatenate the values in the TXT record:

+
+
$ dig mail._domainkey.domain.tld TXT
+---
+;; ANSWER SECTION
+mail._domainkey.<DOMAIN> 300 IN TXT "v=DKIM1; k=rsa; "
+    "p=AZERTYUIOPQSDF..."
+    "asdfQWERTYUIOPQSDF..."
+
+

The target (or value) field must then have all the parts together: v=DKIM1; k=rsa; p=AZERTYUIOPQSDF...asdfQWERTYUIOPQSDF...

+

Verify-Only

+

If you want DKIM to only verify incoming emails, the following version of /etc/opendkim.conf may be useful (right now there is no easy mechanism for installing it other than forking the repo):

+
# This is a simple config file verifying messages only
+
+#LogWhy                 yes
+Syslog                  yes
+SyslogSuccess           yes
+
+Socket                  inet:12301@localhost
+PidFile                 /var/run/opendkim/opendkim.pid
+
+ReportAddress           postmaster@my-domain.com
+SendReports             yes
+
+Mode                    v
+
+

Switch Off DKIM

+

Simply remove the DKIM key by recreating (not just relaunching) the mailserver container.

+

Debugging

+
    +
  • DKIM-verifer: A add-on for the mail client Thunderbird.
  • +
  • You can debug your TXT records with the dig tool.
  • +
+
$ dig TXT mail._domainkey.domain.tld
+---
+; <<>> DiG 9.10.3-P4-Debian <<>> TXT mail._domainkey.domain.tld
+;; global options: +cmd
+;; Got answer:
+;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39669
+;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
+
+;; OPT PSEUDOSECTION:
+; EDNS: version: 0, flags:; udp: 512
+;; QUESTION SECTION:
+;mail._domainkey.domain.tld. IN TXT
+
+;; ANSWER SECTION:
+mail._domainkey.domain.tld. 3600 IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxBSjG6RnWAdU3oOlqsdf2WC0FOUmU8uHVrzxPLW2R3yRBPGLrGO1++yy3tv6kMieWZwEBHVOdefM6uQOQsZ4brahu9lhG8sFLPX4MaKYN/NR6RK4gdjrZu+MYSdfk3THgSbNwIDAQAB"
+
+;; Query time: 50 msec
+;; SERVER: 127.0.1.1#53(127.0.1.1)
+;; WHEN: Wed Sep 07 18:22:57 CEST 2016
+;; MSG SIZE  rcvd: 310
+
+
+
+

Key sizes >=4096-bit

+

Keys of 4096 bits could de denied by some mailservers. According to https://tools.ietf.org/html/rfc6376 keys are preferably between 512 and 2048 bits. See issue #1854.

+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/best-practices/dmarc/index.html b/v10.0/config/best-practices/dmarc/index.html new file mode 100644 index 00000000..3270c1ca --- /dev/null +++ b/v10.0/config/best-practices/dmarc/index.html @@ -0,0 +1,1386 @@ + + + + + + + + + + + + + + + + + + + + + + + Best Practices | DMARC - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + + + +
+
+
+ + + + + + +
+
+
+ + + + + + + + +
+
+ + + + + + + +

DMARC

+ + +

Enabling DMARC

+

In docker-mailserver, DMARC is pre-configured out-of the box. The only thing you need to do in order to enable it, is to add new TXT entry to your DNS.

+

In contrast with DKIM, DMARC DNS entry does not require any keys, but merely setting the configuration values. You can either handcraft the entry by yourself or use one of available generators (like https://dmarcguide.globalcyberalliance.org/).

+

Typically something like this should be good to start with (don't forget to replace @domain.com to your actual domain) +

_dmarc.domain.com. IN TXT "v=DMARC1; p=none; rua=mailto:dmarc.report@domain.com; ruf=mailto:dmarc.report@domain.com; sp=none; ri=86400"
+

+

Or a bit more strict policies (mind p=quarantine and sp=quarantine): +

_dmarc IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc.report@domain.com; ruf=mailto:dmarc.report@domain.com; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; sp=quarantine"
+

+

DMARC status is not being displayed instantly in Gmail for instance. If you want to check it directly after DNS entries, you can use some services around the Internet such as https://dmarcguide.globalcyberalliance.org/ or https://ondmarc.redsift.com/. In other case, email clients will show "DMARC: PASS" in ~1 day or so.

+

Reference: #1511

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/best-practices/spf/index.html b/v10.0/config/best-practices/spf/index.html new file mode 100644 index 00000000..bfca132a --- /dev/null +++ b/v10.0/config/best-practices/spf/index.html @@ -0,0 +1,1423 @@ + + + + + + + + + + + + + + + + + + + + + + + Best Practices | SPF - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + + + +
+
+
+ + + + + + +
+
+
+ + + + + + + + +
+
+ + + + + + + +

SPF

+ +

From Wikipedia:

+
+

Quote

+

Sender Policy Framework (SPF) is a simple email-validation system designed to detect email spoofing by providing a mechanism to allow receiving mail exchangers to check that incoming mail from a domain comes from a host authorized by that domain's administrators. The list of authorized sending hosts for a domain is published in the Domain Name System (DNS) records for that domain in the form of a specially formatted TXT record. Email spam and phishing often use forged "from" addresses, so publishing and checking SPF records can be considered anti-spam techniques.

+
+ +

Add a SPF Record

+

To add a SPF record in your DNS, insert the following line in your DNS zone:

+
; MX record must be declared for SPF to work
+domain.com. IN  MX 1 mail.domain.com.
+
+; SPF record
+domain.com. IN TXT "v=spf1 mx ~all" 
+
+

This enables the Softfail mode for SPF. You could first add this SPF record with a very low TTL.
+SoftFail is a good setting for getting started and testing, as it lets all email through, with spams tagged as such in the mailbox.

+

After verification, you might want to change your SPF record to v=spf1 mx -all so as to enforce the HardFail policy. See http://www.open-spf.org/SPF_Record_Syntax for more details about SPF policies.

+

In any case, increment the SPF record's TTL to its final value.

+

Backup MX, Secondary MX

+

For whitelisting a IP Address from the SPF test, you can create a config file (see policyd-spf.conf) and mount that file into /etc/postfix-policyd-spf-python/policyd-spf.conf.

+

Example:

+

Create and edit a policyd-spf.conf file here /<your docker-mailserver dir>/config/postfix-policyd-spf.conf:

+
debugLevel = 1
+#0(only errors)-4(complete data received)
+
+skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1
+
+# Preferably use IP-Addresses for whitelist lookups:
+Whitelist = 192.168.0.0/31,192.168.1.0/30
+# Domain_Whitelist = mx1.mybackupmx.com,mx2.mybackupmx.com
+
+

Then add this line to docker-compose.yml:

+
volumes:
+  - ./config/postfix-policyd-spf.conf:/etc/postfix-policyd-spf-python/policyd-spf.conf
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/environment/index.html b/v10.0/config/environment/index.html new file mode 100644 index 00000000..21e6b03e --- /dev/null +++ b/v10.0/config/environment/index.html @@ -0,0 +1,3748 @@ + + + + + + + + + + + + + + + + + + + + + + + Environment Variables - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Environment Variables

+ +
+

Info

+

Values in bold are the default values. If an option doesn't work as documented here, check if you are running the latest image. The current master branch corresponds to the image mailserver/docker-mailserver:edge.

+
+

General

+
OVERRIDE_HOSTNAME
+
    +
  • empty => uses the hostname command to get the mail server's canonical hostname.
  • +
  • => Specify a fully-qualified domainname to serve mail for. This is used for many of the config features so if you can't set your hostname (e.g. you're in a container platform that doesn't let you) specify it in this environment variable.
  • +
+
DMS_DEBUG
+
    +
  • 0 => Debug disabled
  • +
  • 1 => Enables debug on startup
  • +
+
SUPERVISOR_LOGLEVEL
+

Here you can adjust the log-level for Supervisor. Possible values are

+
    +
  • critical => Only show critical messages
  • +
  • error => Only show erroneous output
  • +
  • warn => Show warnings
  • +
  • info => Normal informational output
  • +
  • debug => Also show debug messages
  • +
+

The log-level will show everything in its class and above.

+
ONE_DIR
+
    +
  • 0 => state in default directories.
  • +
  • 1 => consolidate all states into a single directory (/var/mail-state) to allow persistence using docker volumes.
  • +
+
PERMIT_DOCKER
+

Set different options for mynetworks option (can be overwrite in postfix-main.cf) WARNING: Adding the docker network's gateway to the list of trusted hosts, e.g. using the network or connected-networks option, can create an open relay, for instance if IPv6 is enabled on the host machine but not in Docker.

+
    +
  • empty => localhost only.
  • +
  • host => Add docker host (ipv4 only).
  • +
  • network => Add the docker default bridge network (172.16.0.0/12); WARNING: docker-compose might use others (e.g. 192.168.0.0/16) use PERMIT_DOCKER=connected-networks in this case.
  • +
  • connected-networks => Add all connected docker networks (ipv4 only).
  • +
+

Note: you probably want to set POSTFIX_INET_PROTOCOLS=ipv4 to make it work fine with Docker.

+
ENABLE_AMAVIS
+

Amavis content filter (used for ClamAV & SpamAssassin)

+
    +
  • 0 => Amavis is disabled
  • +
  • 1 => Amavis is enabled
  • +
+
AMAVIS_LOGLEVEL
+

This page provides information on Amavis' logging statistics.

+
    +
  • -1/-2/-3 => Only show errors
  • +
  • 0 => Show warnings
  • +
  • 1/2 => Show default informational output
  • +
  • 3/4/5 => log debug information (very verbose)
  • +
+
ENABLE_CLAMAV
+
    +
  • 0 => Clamav is disabled
  • +
  • 1 => Clamav is enabled
  • +
+
ENABLE_POP3
+
    +
  • empty => POP3 service disabled
  • +
  • 1 => Enables POP3 service
  • +
+
ENABLE_FAIL2BAN
+
    +
  • 0 => fail2ban service disabled
  • +
  • 1 => Enables fail2ban service
  • +
+

If you enable Fail2Ban, don't forget to add the following lines to your docker-compose.yml:

+
cap_add:
+  - NET_ADMIN
+
+

Otherwise, iptables won't be able to ban IPs.

+
FAIL2BAN_BLOCKTYPE
+
    +
  • drop => drop packet (send NO reply)
  • +
  • reject => reject packet (send ICMP unreachable) +FAIL2BAN_BLOCKTYPE=drop
  • +
+
SMTP_ONLY
+
    +
  • empty => all daemons start
  • +
  • 1 => only launch postfix smtp
  • +
+
SSL_TYPE
+
    +
  • empty => SSL disabled.
  • +
  • letsencrypt => Enables Let's Encrypt certificates.
  • +
  • custom => Enables custom certificates.
  • +
  • manual => Let you manually specify locations of your SSL certificates for non-standard cases
  • +
  • Requires: SSL_CERT_PATH and SSL_KEY_PATH ENV vars to be set to the location of the files within the container.
  • +
  • Optional: SSL_ALT_CERT_PATH and SSL_ALT_KEY_PATH allow providing a 2nd certificate as a fallback for dual (aka hybrid) certificate support. Useful for ECDSA with an RSA fallback. Presently only manual mode supports this feature.
  • +
  • self-signed => Enables self-signed certificates.
  • +
+

Please read the SSL page in the documentation for more information.

+
TLS_LEVEL
+
    +
  • empty => modern
  • +
  • modern => Enables TLSv1.2 and modern ciphers only. (default)
  • +
  • intermediate => Enables TLSv1, TLSv1.1 and TLSv1.2 and broad compatibility ciphers.
  • +
+
SPOOF_PROTECTION
+

Configures the handling of creating mails with forged sender addresses.

+
    +
  • empty => Mail address spoofing allowed. Any logged in user may create email messages with a forged sender address. See also Wikipedia(not recommended, but default for backwards compatibility reasons)
  • +
  • 1 => (recommended) Mail spoofing denied. Each user may only send with his own or his alias addresses. Addresses with extension delimiters are not able to send messages.
  • +
+
ENABLE_SRS
+

Enables the Sender Rewriting Scheme. SRS is needed if your mail server acts as forwarder. See postsrsd for further explanation.

+
    +
  • 0 => Disabled
  • +
  • 1 => Enabled
  • +
+
NETWORK_INTERFACE
+

In case your network interface differs from eth0, e.g. when you are using HostNetworking in Kubernetes, you can set this to whatever interface you want. This interface will then be used.

+
    +
  • empty => eth0
  • +
+
VIRUSMAILS_DELETE_DELAY
+

Set how many days a virusmail will stay on the server before being deleted

+
    +
  • empty => 7 days
  • +
+
ENABLE_POSTFIX_VIRTUAL_TRANSPORT
+

This Option is activating the Usage of POSTFIX_DAGENT to specify a ltmp client different from default dovecot socket.

+
    +
  • empty => disabled
  • +
  • 1 => enabled
  • +
+
POSTFIX_DAGENT
+

Enabled by ENABLE_POSTFIX_VIRTUAL_TRANSPORT. Specify the final delivery of postfix

+ +
POSTFIX_MAILBOX_SIZE_LIMIT
+

Set the mailbox size limit for all users. If set to zero, the size will be unlimited (default).

+
    +
  • empty => 0 (no limit)
  • +
+
ENABLE_QUOTAS
+
    +
  • 1 => Dovecot quota is enabled
  • +
  • 0 => Dovecot quota is disabled
  • +
+

See mailbox quota.

+
POSTFIX_MESSAGE_SIZE_LIMIT
+

Set the message size limit for all users. If set to zero, the size will be unlimited (not recommended!)

+
    +
  • empty => 10240000 (~10 MB)
  • +
+
ENABLE_MANAGESIEVE
+
    +
  • empty => Managesieve service disabled
  • +
  • 1 => Enables Managesieve on port 4190
  • +
+
OVERRIDE_HOSTNAME
+
    +
  • empty => uses the hostname command to get the mail server's canonical hostname
  • +
  • => Specify a fully-qualified domainname to serve mail for. This is used for many of the config features so if you can't set your hostname (e.g. you're in a container platform that doesn't let you) specify it in this environment variable.
  • +
+
POSTMASTER_ADDRESS
+ +
ENABLE_UPDATE_CHECK
+

Check for updates on container start and then once a day. If an update is available, a mail is send to POSTMASTER_ADDRESS.

+
    +
  • 0 => Update check disabled
  • +
  • 1 => Update check enabled
  • +
+
UPDATE_CHECK_INTERVAL
+

Customize the update check interval. Number + Suffix. Suffix must be 's' for seconds, 'm' for minutes, 'h' for hours or 'd' for days.

+
    +
  • 1d => Check for updates once a day
  • +
+
POSTSCREEN_ACTION
+
    +
  • enforce => Allow other tests to complete. Reject attempts to deliver mail with a 550 SMTP reply, and log the helo/sender/recipient information. Repeat this test the next time the client connects.
  • +
  • drop => Drop the connection immediately with a 521 SMTP reply. Repeat this test the next time the client connects.
  • +
  • ignore => Ignore the failure of this test. Allow other tests to complete. Repeat this test the next time the client connects. This option is useful for testing and collecting statistics without blocking mail.
  • +
+
DOVECOT_MAILBOX_FORMAT
+
    +
  • maildir => uses very common Maildir format, one file contains one message
  • +
  • sdbox => (experimental) uses Dovecot high-performance mailbox format, one file contains one message
  • +
  • mdbox ==> (experimental) uses Dovecot high-performance mailbox format, multiple messages per file and multiple files per box
  • +
+

This option has been added in November 2019. Using other format than Maildir is considered as experimental in docker-mailserver and should only be used for testing purpose. For more details, please refer to Dovecot Documentation.

+
POSTFIX_INET_PROTOCOLS
+
    +
  • all => All possible protocols.
  • +
  • ipv4 => Use only IPv4 traffic. Most likely you want this behind Docker.
  • +
  • ipv6 => Use only IPv6 traffic.
  • +
+

Note: More details in http://www.postfix.org/postconf.5.html#inet_protocols

+

Reports

+
PFLOGSUMM_TRIGGER
+

Enables regular pflogsumm mail reports.

+
    +
  • not set => No report
  • +
  • daily_cron => Daily report for the previous day
  • +
  • logrotate => Full report based on the mail log when it is rotated
  • +
+

This is a new option. The old REPORT options are still supported for backwards compatibility. +If this is not set and reports are enabled with the old options, logrotate will be used.

+
PFLOGSUMM_RECIPIENT
+

Recipient address for pflogsumm reports.

+
    +
  • not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS
  • +
  • => Specify the recipient address(es)
  • +
+
PFLOGSUMM_SENDER
+

From address for pflogsumm reports.

+
    +
  • not set => Use REPORT_SENDER or POSTMASTER_ADDRESS
  • +
  • => Specify the sender address
  • +
+
LOGWATCH_INTERVAL
+

Interval for logwatch report.

+
    +
  • none => No report is generated
  • +
  • daily => Send a daily report
  • +
  • weekly => Send a report every week
  • +
+
LOGWATCH_RECIPIENT
+

Recipient address for logwatch reports if they are enabled.

+
    +
  • not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS
  • +
  • => Specify the recipient address(es)
  • +
+
REPORT_RECIPIENT (deprecated)
+

Enables a report being sent (created by pflogsumm) on a regular basis.

+
    +
  • 0 => Report emails are disabled unless enabled by other options
  • +
  • 1 => Using POSTMASTER_ADDRESS as the recipient
  • +
  • => Specify the recipient address
  • +
+
REPORT_SENDER (deprecated)
+

Change the sending address for mail report

+
    +
  • empty => mailserver-report@hostname
  • +
  • => Specify the report sender (From) address
  • +
+
REPORT_INTERVAL (deprecated)
+

Changes the interval in which logs are rotated and a report is being sent (deprecated).

+
    +
  • daily => Send a daily report
  • +
  • weekly => Send a report every week
  • +
  • monthly => Send a report every month
  • +
+

Note: This variable used to control logrotate inside the container and sent the pflogsumm report when the logs were rotated. +It is still supported for backwards compatibility, but the new option LOGROTATE_INTERVAL has been added that only rotates +the logs.

+
LOGROTATE_INTERVAL
+

Defines the interval in which the mail log is being rotated.

+
    +
  • daily => Rotate daily.
  • +
  • weekly => Rotate weekly.
  • +
  • monthly => Rotate monthly.
  • +
+

Note that only the log inside the container is affected. +The full log output is still available via docker logs mail (or your respective container name). +If you want to control logrotation for the docker generated logfile see: Docker Logging Drivers.

+

Also note that by default the logs are lost when the container is recycled. To keep the logs, mount a volume.

+

Finally the logrotate interval may affect the period for generated reports. That is the case when the reports are triggered by log rotation.

+

SpamAssassin

+
ENABLE_SPAMASSASSIN
+
    +
  • 0 => SpamAssassin is disabled
  • +
  • 1 => SpamAssassin is enabled
  • +
+

/!\ Spam delivery: when SpamAssassin is enabled, messages marked as spam WILL NOT BE DELIVERED. +Use SPAMASSASSIN_SPAM_TO_INBOX=1 for receiving spam messages.

+
SPAMASSASSIN_SPAM_TO_INBOX
+
    +
  • 0 => Spam messages will be bounced (rejected) without any notification (dangerous).
  • +
  • 1 => Spam messages will be delivered to the inbox and tagged as spam using SA_SPAM_SUBJECT.
  • +
+
MOVE_SPAM_TO_JUNK
+
    +
  • 1 => Spam messages will be delivered in the Junk folder.
  • +
  • 0 => Spam messages will be delivered in the mailbox.
  • +
+

Note: this setting needs SPAMASSASSIN_SPAM_TO_INBOX=1

+
SA_TAG
+
    +
  • 2.0 => add spam info headers if at, or above that level
  • +
+

Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1

+
SA_TAG2
+
    +
  • 6.31 => add 'spam detected' headers at that level
  • +
+

Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1

+
SA_KILL
+
    +
  • 6.31 => triggers spam evasive actions
  • +
+

Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1. By default, the mailserver is configured to quarantine spam emails. If emails are quarantined, they are compressed and stored in a location dependent on the ONE_DIR setting above. If ONE_DIR=1 the location is /var/mail-state/lib-amavis/virusmails/. If ONE_DIR=0 it is /var/lib/amavis/virusmails/. These paths are inside the docker container. To inhibit this behaviour and deliver spam emails, set this to a very high value e.g. 100.0.

+
SA_SPAM_SUBJECT
+
    +
  • ***SPAM*** => add tag to subject if spam detected
  • +
+

Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1. Add the SpamAssassin score to the subject line by inserting the keyword _SCORE_: ***SPAM(_SCORE_)***.

+
SA_SHORTCIRCUIT_BAYES_SPAM
+
    +
  • 1 => will activate SpamAssassin short circuiting for bayes spam detection.
  • +
+

This will uncomment the respective line in /etc/spamassasin/local.cf

+

Note: activate this only if you are confident in your bayes database for identifying spam.

+
SA_SHORTCIRCUIT_BAYES_HAM
+
    +
  • 1 => will activate SpamAssassin short circuiting for bayes ham detection
  • +
+

This will uncomment the respective line in /etc/spamassasin/local.cf

+

Note: activate this only if you are confident in your bayes database for identifying ham.

+

Fetchmail

+
ENABLE_FETCHMAIL
+
    +
  • 0 => fetchmail disabled
  • +
  • 1 => fetchmail enabled
  • +
+
FETCHMAIL_POLL
+
    +
  • 300 => fetchmail The number of seconds for the interval
  • +
+
FETCHMAIL_PARALLEL
+

0 => fetchmail runs with a single config file /etc/fetchmailrc + 1 => /etc/fetchmailrc is split per poll entry. For every poll entry a seperate fetchmail instance is started to allow having multiple imap idle configurations defined.

+

Note: The defaults of your fetchmailrc file need to be at the top of the file. Otherwise it won't be added correctly to all separate fetchmail instances.

+

LDAP

+
ENABLE_LDAP
+
    +
  • empty => LDAP authentification is disabled
  • +
  • 1 => LDAP authentification is enabled
  • +
  • NOTE:
  • +
  • A second container for the ldap service is necessary (e.g. docker-openldap)
  • +
  • For preparing the ldap server to use in combination with this container this article may be helpful
  • +
+
LDAP_START_TLS
+
    +
  • empty => no
  • +
  • yes => LDAP over TLS enabled for Postfix
  • +
+
LDAP_SERVER_HOST
+
    +
  • empty => mail.domain.com
  • +
  • => Specify the dns-name/ip-address where the ldap-server is listening, or an URI like ldaps://mail.domain.com
  • +
  • NOTE: If you going to use the mailserver in combination with docker-compose you can set the service name here
  • +
+
LDAP_SEARCH_BASE
+
    +
  • empty => ou=people,dc=domain,dc=com
  • +
  • => e.g. LDAP_SEARCH_BASE=dc=mydomain,dc=local
  • +
+
LDAP_BIND_DN
+
    +
  • empty => cn=admin,dc=domain,dc=com
  • +
  • => take a look at examples of SASL_LDAP_BIND_DN
  • +
+
LDAP_BIND_PW
+
    +
  • empty => admin
  • +
  • => Specify the password to bind against ldap
  • +
+
LDAP_QUERY_FILTER_USER
+
    +
  • e.g. (&(mail=%s)(mailEnabled=TRUE))
  • +
  • => Specify how ldap should be asked for users
  • +
+
LDAP_QUERY_FILTER_GROUP
+
    +
  • e.g. (&(mailGroupMember=%s)(mailEnabled=TRUE))
  • +
  • => Specify how ldap should be asked for groups
  • +
+
LDAP_QUERY_FILTER_ALIAS
+
    +
  • e.g. (&(mailAlias=%s)(mailEnabled=TRUE))
  • +
  • => Specify how ldap should be asked for aliases
  • +
+
LDAP_QUERY_FILTER_DOMAIN
+
    +
  • e.g. (&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))
  • +
  • => Specify how ldap should be asked for domains
  • +
+
LDAP_QUERY_FILTER_SENDERS
+
    +
  • empty => use user/alias/group maps directly, equivalent to (|($LDAP_QUERY_FILTER_USER)($LDAP_QUERY_FILTER_ALIAS)($LDAP_QUERY_FILTER_GROUP))
  • +
  • => Override how ldap should be asked if a sender address is allowed for a user
  • +
+
DOVECOT_TLS
+
    +
  • empty => no
  • +
  • yes => LDAP over TLS enabled for Dovecot
  • +
+

Dovecot

+

The following variables overwrite the default values for /etc/dovecot/dovecot-ldap.conf.ext.

+
DOVECOT_BASE
+
    +
  • empty => same as LDAP_SEARCH_BASE
  • +
  • => Tell Dovecot to search only below this base entry. (e.g. ou=people,dc=domain,dc=com)
  • +
+
DOVECOT_DEFAULT_PASS_SCHEME
+
    +
  • empty => SSHA
  • +
  • => Select one crypt scheme for password hashing from this list of password schemes.
  • +
+
DOVECOT_DN
+
    +
  • empty => same as LDAP_BIND_DN
  • +
  • => Bind dn for LDAP connection. (e.g. cn=admin,dc=domain,dc=com)
  • +
+
DOVECOT_DNPASS
+
    +
  • empty => same as LDAP_BIND_PW
  • +
  • => Password for LDAP dn sepecifified in DOVECOT_DN.
  • +
+
DOVECOT_URIS
+
    +
  • empty => same as LDAP_SERVER_HOST
  • +
  • => Specify a space separated list of LDAP uris.
  • +
  • Note: If the protocol is missing, ldap:// will be used.
  • +
  • Note: This deprecates DOVECOT_HOSTS (as it didn't allow to use LDAPS), which is currently still supported for backwards compatibility.
  • +
+
DOVECOT_LDAP_VERSION
+
    +
  • empty => 3
  • +
  • 2 => LDAP version 2 is used
  • +
  • 3 => LDAP version 3 is used
  • +
+
DOVECOT_AUTH_BIND
+ +
DOVECOT_USER_FILTER
+
    +
  • e.g. (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
  • +
+
DOVECOT_USER_ATTRS
+
    +
  • e.g. homeDirectory=home,qmailUID=uid,qmailGID=gid,mailMessageStore=mail
  • +
  • => Specify the directory to dovecot attribute mapping that fits your directory structure.
  • +
  • Note: This is necessary for directories that do not use the Postfix Book Schema.
  • +
  • Note: The left-hand value is the directory attribute, the right hand value is the dovecot variable.
  • +
  • More details on the Dovecot Wiki
  • +
+
DOVECOT_PASS_FILTER
+
    +
  • e.g. (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
  • +
  • empty => same as DOVECOT_USER_FILTER
  • +
+
DOVECOT_PASS_ATTRS
+
    +
  • e.g. uid=user,userPassword=password
  • +
  • => Specify the directory to dovecot variable mapping that fits your directory structure.
  • +
  • Note: This is necessary for directories that do not use the Postfix Book Schema.
  • +
  • Note: The left-hand value is the directory attribute, the right hand value is the dovecot variable.
  • +
  • More details on the Dovecot Wiki
  • +
+

Postgrey

+
ENABLE_POSTGREY
+
    +
  • 0 => postgrey is disabled
  • +
  • 1 => postgrey is enabled
  • +
+
POSTGREY_DELAY
+
    +
  • 300 => greylist for N seconds
  • +
+

Note: This postgrey setting needs ENABLE_POSTGREY=1

+
POSTGREY_MAX_AGE
+
    +
  • 35 => delete entries older than N days since the last time that they have been seen
  • +
+

Note: This postgrey setting needs ENABLE_POSTGREY=1

+
POSTGREY_AUTO_WHITELIST_CLIENTS
+
    +
  • 5 => whitelist host after N successful deliveries (N=0 to disable whitelisting)
  • +
+

Note: This postgrey setting needs ENABLE_POSTGREY=1

+
POSTGREY_TEXT
+
    +
  • Delayed by Postgrey => response when a mail is greylisted
  • +
+

Note: This postgrey setting needs ENABLE_POSTGREY=1

+

SASL Auth

+
ENABLE_SASLAUTHD
+
    +
  • 0 => saslauthd is disabled
  • +
  • 1 => saslauthd is enabled
  • +
+
SASLAUTHD_MECHANISMS
+
    +
  • empty => pam
  • +
  • ldap => authenticate against ldap server
  • +
  • shadow => authenticate against local user db
  • +
  • mysql => authenticate against mysql db
  • +
  • rimap => authenticate against imap server
  • +
  • NOTE: can be a list of mechanisms like pam ldap shadow
  • +
+
SASLAUTHD_MECH_OPTIONS
+
    +
  • empty => None
  • +
  • e.g. with SASLAUTHD_MECHANISMS rimap you need to specify the ip-address/servername of the imap server ==> xxx.xxx.xxx.xxx
  • +
+
SASLAUTHD_LDAP_SERVER
+
    +
  • empty => same as LDAP_SERVER_HOST
  • +
  • Note: since version 10.0.0, you can specify a protocol here (like ldaps://); this deprecates SASLAUTHD_LDAP_SSL.
  • +
+
SASLAUTHD_LDAP_START_TLS
+
    +
  • empty => no
  • +
  • yes => Enable ldap_start_tls option
  • +
+
SASLAUTHD_LDAP_TLS_CHECK_PEER
+
    +
  • empty => no
  • +
  • yes => Enable ldap_tls_check_peer option
  • +
+
SASLAUTHD_LDAP_TLS_CACERT_DIR
+

Path to directory with CA (Certificate Authority) certificates.

+
    +
  • empty => Nothing is added to the configuration
  • +
  • Any value => Fills the ldap_tls_cacert_dir option
  • +
+
SASLAUTHD_LDAP_TLS_CACERT_FILE
+

File containing CA (Certificate Authority) certificate(s).

+
    +
  • empty => Nothing is added to the configuration
  • +
  • Any value => Fills the ldap_tls_cacert_file option
  • +
+
SASLAUTHD_LDAP_BIND_DN
+
    +
  • empty => same as LDAP_BIND_DN
  • +
  • specify an object with privileges to search the directory tree
  • +
  • e.g. active directory: SASLAUTHD_LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=net
  • +
  • e.g. openldap: SASLAUTHD_LDAP_BIND_DN=cn=admin,dc=mydomain,dc=net
  • +
+
SASLAUTHD_LDAP_PASSWORD
+
    +
  • empty => same as LDAP_BIND_PW
  • +
+
SASLAUTHD_LDAP_SEARCH_BASE
+
    +
  • empty => same as LDAP_SEARCH_BASE
  • +
  • specify the search base
  • +
+
SASLAUTHD_LDAP_FILTER
+
    +
  • empty => default filter (&(uniqueIdentifier=%u)(mailEnabled=TRUE))
  • +
  • e.g. for active directory: (&(sAMAccountName=%U)(objectClass=person))
  • +
  • e.g. for openldap: (&(uid=%U)(objectClass=person))
  • +
+
SASLAUTHD_LDAP_PASSWORD_ATTR
+

Specify what password attribute to use for password verification.

+
    +
  • empty => Nothing is added to the configuration but the documentation says it is userPassword by default.
  • +
  • Any value => Fills the ldap_password_attr option
  • +
+
SASL_PASSWD
+
    +
  • empty => No sasl_passwd will be created
  • +
  • string => /etc/postfix/sasl_passwd will be created with the string as password
  • +
+
SASLAUTHD_LDAP_AUTH_METHOD
+
    +
  • empty => bind will be used as a default value
  • +
  • fastbind => The fastbind method is used
  • +
  • custom => The custom method uses userPassword attribute to verify the password
  • +
+
SASLAUTHD_LDAP_MECH
+

Specify the authentication mechanism for SASL bind.

+
    +
  • empty => Nothing is added to the configuration
  • +
  • Any value => Fills the ldap_mech option
  • +
+

SRS (Sender Rewriting Scheme)

+
SRS_SENDER_CLASSES
+

An email has an "envelope" sender (indicating the sending server) and a +"header" sender (indicating who sent it). More strict SPF policies may require +you to replace both instead of just the envelope sender.

+

More info.

+
    +
  • envelope_sender => Rewrite only envelope sender address
  • +
  • header_sender => Rewrite only header sender (not recommended)
  • +
  • envelope_sender,header_sender => Rewrite both senders
  • +
+
SRS_EXCLUDE_DOMAINS
+
    +
  • empty => Envelope sender will be rewritten for all domains
  • +
  • provide comma separated list of domains to exclude from rewriting
  • +
+
SRS_SECRET
+
    +
  • empty => generated when the container is started for the first time
  • +
  • provide a secret to use in base64
  • +
  • you may specify multiple keys, comma separated. the first one is used for signing and the remaining will be used for verification. this is how you rotate and expire keys
  • +
  • if you have a cluster/swarm make sure the same keys are on all nodes
  • +
  • example command to generate a key: dd if=/dev/urandom bs=24 count=1 2>/dev/null | base64
  • +
+
SRS_DOMAINNAME
+
    +
  • empty => Derived from OVERRIDE_HOSTNAME, DOMAINNAME, or the container's hostname
  • +
  • Set this if auto-detection fails, isn't what you want, or you wish to have a separate container handle DSNs
  • +
+

Default Relay Host

+
DEFAULT_RELAY_HOST
+
    +
  • empty => don't set default relayhost setting in main.cf
  • +
  • default host and port to relay all mail through. + Format: [example.com]:587 (don't forget the brackets if you need this to + be compatible with $RELAY_USER and $RELAY_PASSWORD, explained below).
  • +
+

Multi-domain Relay Hosts

+
RELAY_HOST
+
    +
  • empty => don't configure relay host
  • +
  • default host to relay mail through
  • +
+
RELAY_PORT
+
    +
  • empty => 25
  • +
  • default port to relay mail through
  • +
+
RELAY_USER
+
    +
  • empty => no default
  • +
  • default relay username (if no specific entry exists in postfix-sasl-password.cf)
  • +
+
RELAY_PASSWORD
+
    +
  • empty => no default
  • +
  • password for default relay user
  • +
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/pop3/index.html b/v10.0/config/pop3/index.html new file mode 100644 index 00000000..a64347d6 --- /dev/null +++ b/v10.0/config/pop3/index.html @@ -0,0 +1,1316 @@ + + + + + + + + + + + + + + + + + + + + + + + Mail Delivery with POP3 - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+ + + + + + + +

Mail Delivery with POP3

+ +

If you want to use POP3(S), you have to add the ports 110 and/or 995 (TLS secured) and the environment variable ENABLE_POP3 to your docker-compose.yml:

+
mail:
+  ports:
+    - "25:25"
+    - "143:143"
+    - "587:587"
+    - "993:993"
+    - "110:110"
+    - "995:995" 
+  environment:
+    - ENABLE_POP3=1
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/security/fail2ban/index.html b/v10.0/config/security/fail2ban/index.html new file mode 100644 index 00000000..2bf65100 --- /dev/null +++ b/v10.0/config/security/fail2ban/index.html @@ -0,0 +1,1324 @@ + + + + + + + + + + + + + + + + + + + + + + + Security | Fail2Ban - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+ + + + + + + +

Fail2Ban

+ +

Fail2Ban is installed automatically and bans IP addresses for 3 hours after 3 failed attempts in 10 minutes by default. If you want to change this, you can easily edit config/fail2ban-jail.cf.

+

You can do the same with the values from fail2ban.conf, e.g dbpurgeage. In that case you need to edit config/fail2ban-fail2ban.cf.

+
+

Attention

+

The mail container must be launched with the NET_ADMIN capability in order to be able to install the iptable rules that actually ban IP addresses.

+

Thus either include --cap-add=NET_ADMIN in the docker run commandline or the equivalent docker-compose.yml:

+
cap_add:
+  - NET_ADMIN
+
+
+

If you don't you will see errors the form of:

+
iptables -w -X f2b-postfix -- stderr: "getsockopt failed strangely: Operation not permitted\niptables v1.4.21: can't initialize iptabl
+es table `filter': Permission denied (you must be root)\nPerhaps iptables or your kernel needs to be upgraded.\niptables v1.4.21: can'
+t initialize iptables table `filter': Permission denied (you must be root)\nPerhaps iptables or your kernel needs to be upgraded.\n"
+2016-06-01 00:53:51,284 fail2ban.action         [678]: ERROR   iptables -w -D INPUT -p tcp -m multiport --dports smtp,465,submission -
+j f2b-postfix
+
+

You can also manage and list the banned IPs with the setup.sh script.

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/security/mail_crypt/index.html b/v10.0/config/security/mail_crypt/index.html new file mode 100644 index 00000000..e18b6351 --- /dev/null +++ b/v10.0/config/security/mail_crypt/index.html @@ -0,0 +1,1433 @@ + + + + + + + + + + + + + + + + + + + + + + + Security | mail_crypt (email/storage encryption) - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Mail Encryption

+ +
+

Info

+

The Mail crypt plugin is used to secure email messages stored in a Dovecot system. Messages are encrypted before written to storage and decrypted after reading. Both operations are transparent to the user.

+

In case of unauthorized access to the storage backend, the messages will, without access to the decryption keys, be unreadable to the offending party.

+

There can be a single encryption key for the whole system or each user can have a key of their own. The used cryptographical methods are widely used standards and keys are stored in portable formats, when possible.

+
+

Official Dovecot documentation: https://doc.dovecot.org/configuration_manual/mail_crypt_plugin/

+
+

Basic Setup

+
    +
  1. Before you can enable mail_crypt, you'll need to copy out several dovecot/conf.d files to the host (from a running container) and then take the container down: +
    mkdir -p config/dovecot
    +docker cp mailserver:/etc/dovecot/conf.d/20-lmtp.conf config/dovecot/
    +docker cp mailserver:/etc/dovecot/conf.d/20-imap.conf config/dovecot/
    +docker cp mailserver:/etc/dovecot/conf.d/20-pop3.conf config/dovecot/
    +docker-compose down
    +
  2. +
  3. You then need to generate your global EC key.
  4. +
  5. The EC key needs to be available in the container. I prefer to mount a /certs directory into the container: +
    services:
    +  mailserver:
    +    image: docker.io/mailserver/docker-mailserver:latest
    +    volumes:
    +    . . .
    +      - ./certs/:/certs
    +    . . .
    +
  6. +
  7. While you're editing the docker-compose.yml, add the configuration files you copied out: +
    services:
    +  mailserver:
    +    image: docker.io/mailserver/docker-mailserver:latest
    +    volumes:
    +    . . .
    +      - ./config/dovecot/20-lmtp.conf:/etc/dovecot/conf.d/20-lmtp.conf
    +      - ./config/dovecot/20-imap.conf:/etc/dovecot/conf.d/20-imap.conf
    +      - ./config/dovecot/20-pop3.conf:/etc/dovecot/conf.d/20-pop3.conf
    +      - ./certs/:/certs
    +    . . .
    +
  8. +
  9. The mail_crypt plugin, unless you're using a non-standard configuration of docker-mailserver, should be enabled on both lmtp and imap. You'll want to edit three different files:
      +
    • ./config/dovecot/20-lmtp.conf +
      protocol lmtp {
      +  mail_plugins = $mail_plugins sieve mail_crypt
      +  plugin {
      +    mail_crypt_global_private_key = </certs/ecprivkey.pem
      +    mail_crypt_global_public_key = </certs/ecpubkey.pem
      +    mail_crypt_save_version = 2
      +  }
      +}
      +
    • +
    • ./config/dovecot/20-imap.conf +
      protocol imap {
      +  mail_plugins = $mail_plugins imap_quota mail_crypt
      +  plugin {
      +    mail_crypt_global_private_key = </certs/ecprivkey.pem
      +    mail_crypt_global_public_key = </certs/ecpubkey.pem
      +    mail_crypt_save_version = 2
      +  }
      +}
      +
    • +
    • If you use pop3, make the same changes in 20-pop3.conf
    • +
    +
  10. +
  11. Start the container and monitor the logs for any errors
  12. +
+

This should be the minimum required for encryption of the mail while in storage.

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/security/ssl/index.html b/v10.0/config/security/ssl/index.html new file mode 100644 index 00000000..f4a6849d --- /dev/null +++ b/v10.0/config/security/ssl/index.html @@ -0,0 +1,2112 @@ + + + + + + + + + + + + + + + + + + + + + + + Security | TLS (aka SSL) - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + +
+
+ + + + + + + +

SSL/TLS

+ +

There are multiple options to enable SSL:

+ +

After installation, you can test your setup with:

+ + +

To enable Let's Encrypt on your mail server, you have to:

+
    +
  • Get your certificate using letsencrypt client
  • +
  • Add an environment variable SSL_TYPE with value letsencrypt (see docker-compose.yml)
  • +
  • Mount your whole letsencrypt folder to /etc/letsencrypt
  • +
  • +

    The certs folder name located in letsencrypt/live/ must be the fqdn of your container responding to the hostname command. The fqdn (full qualified domain name) inside the docker container is built combining the hostname and domainname values of the docker-compose file, eg:

    +
    services:
    +  mailserver:
    +    hostname: mail
    +    domainname: myserver.tld
    +    fqdn: mail.myserver.tld
    +
    +
  • +
+

You don't have anything else to do. Enjoy.

+

Example using Docker for Let's Encrypt

+
    +
  1. +

    Make a directory to store your letsencrypt logs and configs. In my case:

    +
    mkdir -p /home/ubuntu/docker/letsencrypt 
    +cd /home/ubuntu/docker/letsencrypt
    +
    +
  2. +
  3. +

    Now get the certificate (modify mail.myserver.tld) and following the certbot instructions.

    +
  4. +
  5. +

    This will need access to port 80 from the internet, adjust your firewall if needed:

    +
    docker run --rm -it \
    +  -v $PWD/log/:/var/log/letsencrypt/ \
    +  -v $PWD/etc/:/etc/letsencrypt/ \
    +  -p 80:80 \
    +  certbot/certbot certonly --standalone -d mail.myserver.tld
    +
    +
  6. +
  7. +

    You can now mount /home/ubuntu/docker/letsencrypt/etc/ in /etc/letsencrypt of docker-mailserver.

    +

    To renew your certificate just run (this will need access to port 443 from the internet, adjust your firewall if needed):

    +
    docker run --rm -it \
    +  -v $PWD/log/:/var/log/letsencrypt/ \
    +  -v $PWD/etc/:/etc/letsencrypt/ \
    +  -p 80:80 \
    +  -p 443:443 \
    +  certbot/certbot renew
    +
    +
  8. +
+

Example using Docker, nginx-proxy and letsencrypt-nginx-proxy-companion

+

If you are running a web server already, it is non-trivial to generate a Let's Encrypt certificate for your mail server using certbot, because port 80 is already occupied. In the following example, we show how docker-mailserver can be run alongside the docker containers nginx-proxy and letsencrypt-nginx-proxy-companion.

+

There are several ways to start nginx-proxy and letsencrypt-nginx-proxy-companion. Any method should be suitable here.

+

For example start nginx-proxy as in the letsencrypt-nginx-proxy-companion documentation:

+
docker run --detach \
+  --name nginx-proxy \
+  --restart always \
+  --publish 80:80 \
+  --publish 443:443 \
+  --volume /server/letsencrypt/etc:/etc/nginx/certs:ro \
+  --volume /etc/nginx/vhost.d \
+  --volume /usr/share/nginx/html \
+  --volume /var/run/docker.sock:/tmp/docker.sock:ro \
+  jwilder/nginx-proxy
+
+

Then start nginx-proxy-letsencrypt:

+
docker run --detach \
+  --name nginx-proxy-letsencrypt \
+  --restart always \
+  --volume /server/letsencrypt/etc:/etc/nginx/certs:rw \
+  --volumes-from nginx-proxy \
+  --volume /var/run/docker.sock:/var/run/docker.sock:ro \
+  jrcs/letsencrypt-nginx-proxy-companion
+
+

Start the rest of your web server containers as usual.

+

Start another container for your mail.myserver.tld. This will generate a Let's Encrypt certificate for your domain, which can be used by docker-mailserver. It will also run a web server on port 80 at that address:

+
docker run -d \
+  --name webmail \
+  -e "VIRTUAL_HOST=mail.myserver.tld" \
+  -e "LETSENCRYPT_HOST=mail.myserver.tld" \
+  -e "LETSENCRYPT_EMAIL=foo@bar.com" \
+  library/nginx
+
+

You may want to add -e LETSENCRYPT_TEST=true to the above while testing to avoid the Let's Encrypt certificate generation rate limits.

+

Finally, start the mailserver with the docker-compose.yml. Make sure your mount path to the letsencrypt certificates is correct.

+

Inside your /path/to/mailserver/docker-compose.yml (for the mailserver from this repo) make sure volumes look like below example:

+
volumes:
+  - maildata:/var/mail
+  - mailstate:/var/mail-state
+  - ./config/:/tmp/docker-mailserver/
+  - /server/letsencrypt/etc:/etc/letsencrypt/live
+
+

Then: /path/to/mailserver/docker-compose up -d mail

+

Example using Docker, nginx-proxy and letsencrypt-nginx-proxy-companion with docker-compose

+

The following docker-compose.yml is the basic setup you need for using letsencrypt-nginx-proxy-companion. It is mainly derived from its own wiki/documenation.

+
Example Code
version: "2"
+
+services:
+  nginx: 
+    image: nginx
+    container_name: nginx
+    ports:
+      - 80:80
+      - 443:443
+    volumes:
+      - /mnt/data/nginx/htpasswd:/etc/nginx/htpasswd
+      - /mnt/data/nginx/conf.d:/etc/nginx/conf.d
+      - /mnt/data/nginx/vhost.d:/etc/nginx/vhost.d
+      - /mnt/data/nginx/html:/usr/share/nginx/html
+      - /mnt/data/nginx/certs:/etc/nginx/certs:ro
+    networks:
+      - proxy-tier
+    restart: always
+
+  nginx-gen:
+    image: jwilder/docker-gen
+    container_name: nginx-gen
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - /mnt/data/nginx/templates/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro
+    volumes_from:
+      - nginx
+    entrypoint: /usr/local/bin/docker-gen -notify-sighup nginx -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
+    restart: always
+
+  letsencrypt-nginx-proxy-companion:
+    image: jrcs/letsencrypt-nginx-proxy-companion
+    container_name: letsencrypt-companion
+    volumes_from:
+      - nginx
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock:ro
+      - /mnt/data/nginx/certs:/etc/nginx/certs:rw
+    environment:
+      - NGINX_DOCKER_GEN_CONTAINER=nginx-gen
+      - DEBUG=false
+    restart: always
+
+networks:
+  proxy-tier:
+    external:
+      name: nginx-proxy
+
+
+

The second part of the setup is the actual mail container. So, in another folder, create another docker-compose.yml with the following content (Removed all ENV variables for this example):

+
Example Code
version: '2'
+services:
+  mailserver:
+    image: mailserver/docker-mailserver:latest
+    hostname: <HOSTNAME> # <-- change this
+    domainname: <DOMAINNAME> # <-- change this
+    container_name: mailserver
+    ports:
+      - "25:25"
+      - "143:143"
+      - "465:465"
+      - "587:587"
+      - "993:993"
+    volumes:
+      - ./mail:/var/mail
+      - ./mail-state:/var/mail-state
+      - ./config/:/tmp/docker-mailserver/
+      - /mnt/data/nginx/certs/:/etc/letsencrypt/live/:ro
+    cap_add:
+      - NET_ADMIN
+      - SYS_PTRACE
+    restart: always
+
+  cert-companion:
+    image: nginx
+    environment:
+      - "VIRTUAL_HOST="
+      - "VIRTUAL_NETWORK=nginx-proxy"
+      - "LETSENCRYPT_HOST="
+      - "LETSENCRYPT_EMAIL="
+    networks:
+      - proxy-tier
+    restart: always
+
+networks:
+  proxy-tier:
+    external:
+      name: nginx-proxy
+
+
+

The mail container needs to have the letsencrypt certificate folder mounted as a volume. No further changes are needed. The second container is a dummy-sidecar we need, because the mail-container do not expose any web-ports. Set your ENV variables as you need. (VIRTUAL_HOST and LETSENCRYPT_HOST are mandandory, see documentation)

+

Example using the Let's Encrypt Certificates on a Synology NAS

+

Version 6.2 and later of the Synology NAS DSM OS now come with an interface to generate and renew letencrypt certificates. Navigation into your DSM control panel and go to Security, then click on the tab Certificate to generate and manage letsencrypt certificates.

+

Amongst other things, you can use these to secure your mail server. DSM locates the generated certificates in a folder below /usr/syno/etc/certificate/_archive/.

+

Navigate to that folder and note the 6 character random folder name of the certificate you'd like to use. Then, add the following to your docker-compose.yml declaration file:

+
volumes:
+  - /usr/syno/etc/certificate/_archive/<your-folder>/:/tmp/ssl
+environment:
+  - SSL_TYPE=manual
+  - SSL_CERT_PATH=/tmp/ssl/fullchain.pem
+  - SSL_KEY_PATH=/tmp/ssl/privkey.pem
+
+

DSM-generated letsencrypt certificates get auto-renewed every three months.

+

Caddy

+

If you are using Caddy to renew your certificates, please note that only RSA certificates work. Read #1440 for details. In short for Caddy v1 the Caddyfile should look something like:

+
https://mail.domain.com {
+  tls yourcurrentemail@gmail.com {
+    key_type rsa2048
+  }
+}
+
+

For Caddy v2 you can specify the key_type in your server's global settings, which would end up looking something like this if you're using a Caddyfile:

+
{
+  debug
+  admin localhost:2019
+  http_port 80
+  https_port 443
+  default_sni mywebserver.com
+  key_type rsa4096
+}
+
+

If you are instead using a json config for Caddy v2, you can set it in your site's TLS automation policies:

+
Example Code
{
+  "apps": {
+    "http": {
+      "servers": {
+        "srv0": {
+          "listen": [
+            ":443"
+          ],
+          "routes": [
+            {
+              "match": [
+                {
+                  "host": [
+                    "mail.domain.com",
+                  ]
+                }
+              ],
+              "handle": [
+                {
+                  "handler": "subroute",
+                  "routes": [
+                    {
+                      "handle": [
+                        {
+                          "body": "",
+                          "handler": "static_response"
+                        }
+                      ]
+                    }
+                  ]
+                }
+              ],
+              "terminal": true
+            },
+          ]
+        }
+      }
+    },
+    "tls": {
+      "automation": {
+        "policies": [
+          {
+            "subjects": [
+              "mail.domain.com",
+            ],
+            "key_type": "rsa2048",
+            "issuer": {
+              "email": "email@email.com",
+              "module": "acme"
+            }
+          },
+          {
+            "issuer": {
+              "email": "email@email.com",
+              "module": "acme"
+            }
+          }
+        ]
+      }
+    }
+  }
+}
+
+
+

The generated certificates can be mounted:

+
volumes:
+  - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.domain.com/mail.domain.com.crt:/etc/letsencrypt/live/mail.domain.com/fullchain.pem
+  - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.domain.com/mail.domain.com.key:/etc/letsencrypt/live/mail.domain.com/privkey.pem
+
+

EC certificates fail in the TLS handshake:

+
CONNECTED(00000003)
+140342221178112:error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure:ssl/record/rec_layer_s3.c:1543:SSL alert number 40
+no peer certificate available
+No client certificate CA names sent
+
+

Traefik v2

+

Traefik is an open-source application proxy using the ACME protocol. Traefik can request certificates for domains and subdomains, and it will take care of renewals, challenge negotiations, etc. We strongly recommend to use Traefik's major version 2.

+

Traefik's storage format is natively supported if the acme.json store is mounted into the container at /etc/letsencrypt/acme.json. The file is also monitored for changes and will trigger a reload of the mail services. Wild card certificates issued for *.domain.tld are supported. You will then want to use SSL_DOMAIN=domain.tld. Lookup of the certificate domain happens in the following order:

+
    +
  1. ${SSL_DOMAIN}
  2. +
  3. ${HOSTNAME}
  4. +
  5. ${DOMAINNAME}
  6. +
+

This setup only comes with one caveat: The domain has to be configured on another service for Traefik to actually request it from Let'sEncrypt, i.e. Traefik will not issue a certificate without a service / router demanding it.

+
Example Code

Here is an example setup for docker-compose:

+
version: '3.8'
+
+services:
+
+  mailserver:
+    image: docker.io/mailserver/docker-mailserver:latest
+    container_name: mailserver
+    hostname: mail
+    domainname: domain.tld
+    volumes:
+       - /traefik/acme.json:/etc/letsencrypt/acme.json:ro
+    environment:
+      SSL_TYPE: letsencrypt
+      SSL_DOMAIN: mail.example.com"
+      # for a wildcard certificate, use
+      # SSL_DOMAIN: example.com
+
+  traefik:
+    image: docker.io/traefik:v2.4.8
+    ports:
+       - "80:80"
+       - "443:443"
+    command:
+       - --providers.docker
+       - --entrypoints.http.address=:80
+       - --entrypoints.http.http.redirections.entryPoint.to=https
+       - --entrypoints.http.http.redirections.entryPoint.scheme=https
+       - --entrypoints.https.address=:443
+       - --entrypoints.https.http.tls.certResolver=letsencrypt
+       - --certificatesresolvers.letsencrypt.acme.email=admin@domain.tld
+       - --certificatesresolvers.letsencrypt.acme.storage=/acme.json
+       - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http
+    volumes:
+       - /traefik/acme.json:/acme.json
+       - /var/run/docker.sock:/var/run/docker.sock:ro
+
+  whoami:
+    image: docker.io/traefik/whoami:latest 
+    labels:
+       - "traefik.http.routers.whoami.rule=Host(`mail.domain.tld`)"
+
+
+

Self-Signed Certificates

+
+

Warning

+

Use self-signed certificates only for testing purposes!

+
+

This feature requires you to provide the following files into your config/ssl/ directory (internal location: /tmp/docker-mailserver/ssl/):

+
    +
  • ${HOSTNAME}-key.pem
  • +
  • ${HOSTNAME}-cert.pem
  • +
  • demoCA/cacert.pem
  • +
+

Where ${HOSTNAME} is the mailserver FQDN (hostname(mail) + domainname(example.com), eg: mail.example.com).

+

To use the certificate:

+
    +
  • Add SSL_TYPE=self-signed to your container environment variables.
  • +
  • If a matching certificate (files listed above) is found in config/ssl, it will be automatically setup in postfix and dovecot. You just have to place them in config/ssl folder.
  • +
+

Generating a self-signed certificate

+
+

Note

+

Since v10, support in setup.sh for generating a self-signed SSL certificate internally was removed.

+

It is now similar to SSL_TYPE=manual (except manual does not support verification for a custom CA), but does not require additional ENV vars for providing the location of cert files.

+
+

One way to generate self-signed certificates is with Smallstep's step CLI. This is exactly what docker-mailserver does for creating test certificates.

+

For example with the FQDN mail.example.test, you can generate the required files by running:

+
#! /bin/sh
+mkdir -p demoCA
+
+step certificate create "Smallstep Root CA" "demoCA/cacert.pem" "demoCA/cakey.pem" \
+  --no-password --insecure \
+  --profile root-ca \
+  --not-before "2021-01-01T00:00:00+00:00" \
+  --not-after "2031-01-01T00:00:00+00:00" \
+  --san "example.test" \
+  --san "mail.example.test" \
+  --kty RSA --size 2048
+
+step certificate create "Smallstep Leaf" mail.example.test-cert.pem mail.example.test-key.pem \
+  --no-password --insecure \
+  --profile leaf \
+  --ca "demoCA/cacert.pem" \
+  --ca-key "demoCA/cakey.pem" \
+  --not-before "2021-01-01T00:00:00+00:00" \
+  --not-after "2031-01-01T00:00:00+00:00" \
+  --san "example.test" \
+  --san "mail.example.test" \
+  --kty RSA --size 2048
+
+

If you'd rather not install the CLI tool locally to run the step commands above; you can save the script above to a file such as generate-certs.sh (and make it executable chmod +x generate-certs.sh) in a directory that you want the certs to be placed, then run that script with docker:

+
# --user to keep ownership of the files to your user and group ID
+docker run --rm -it \
+  --user "$(id -u):$(id -g)" \
+  --volume "${PWD}:/tmp" \
+  --workdir "/tmp" \
+  --entrypoint "/tmp/generate-certs.sh" \
+  smallstep/step-ca
+
+

Custom Certificate Files

+

You can also provide your own certificate files. Add these entries to your docker-compose.yml:

+
volumes:
+  - /etc/ssl:/tmp/ssl:ro
+environment:
+  - SSL_TYPE=manual
+  - SSL_CERT_PATH=/tmp/ssl/cert/public.crt
+  - SSL_KEY_PATH=/tmp/ssl/private/private.key
+
+

This will mount the path where your ssl certificates reside as read-only under /tmp/ssl. Then all you have to do is to specify the location of your private key and the certificate.

+
+

Info

+

You may have to restart your mailserver once the certificates change.

+
+

Testing a Certificate is Valid

+
    +
  • +

    From your host:

    +
    docker exec mail openssl s_client \
    +  -connect 0.0.0.0:25 \
    +  -starttls smtp \
    +  -CApath /etc/ssl/certs/
    +
    +
  • +
  • +

    Or:

    +
    docker exec mail openssl s_client \
    +  -connect 0.0.0.0:143 \
    +  -starttls imap \
    +  -CApath /etc/ssl/certs/
    +
    +
  • +
+

And you should see the certificate chain, the server certificate and: Verify return code: 0 (ok)

+

In addition, to verify certificate dates:

+
docker exec mail openssl s_client \
+  -connect 0.0.0.0:25 \
+  -starttls smtp \
+  -CApath /etc/ssl/certs/ \
+  2>/dev/null | openssl x509 -noout -dates
+
+

Plain-Text Access

+
+

Warning

+

Not recommended for purposes other than testing.

+
+

Add this to config/dovecot.cf:

+
ssl = yes
+disable_plaintext_auth=no
+
+

These options in conjunction mean:

+
    +
  • SSL/TLS is offered to the client, but the client isn't required to use it.
  • +
  • The client is allowed to login with plaintext authentication even when SSL/TLS isn't enabled on the connection.
  • +
  • This is insecure, because the plaintext password is exposed to the internet.
  • +
+

Importing Certificates Obtained via Another Source

+

If you have another source for SSL/TLS certificates you can import them into the server via an external script. The external script can be found here: external certificate import script.

+

The steps to follow are these:

+
    +
  1. Transport the new certificates to ./config/ssl (/tmp/ssl in the container)
  2. +
  3. You should provide fullchain.key and privkey.pem
  4. +
  5. Place the script in ./config/ (or /tmp/docker-mailserver/ inside the container)
  6. +
  7. Make the script executable (chmod +x tomav-renew-certs.sh)
  8. +
  9. Run the script: docker exec mail /tmp/docker-mailserver/tomav-renew-certs.sh
  10. +
+

If an error occurs the script will inform you. If not you will see both postfix and dovecot restart.

+

After the certificates have been loaded you can check the certificate:

+
openssl s_client \
+  -servername mail.mydomain.net \
+  -connect 192.168.0.72:465 \
+  2>/dev/null | openssl x509
+
+# or
+
+openssl s_client \
+  -servername mail.mydomain.net \
+  -connect mail.mydomain.net:465 \
+  2>/dev/null | openssl x509
+
+

Or you can check how long the new certificate is valid with commands like:

+
export SITE_URL="mail.mydomain.net"
+export SITE_IP_URL="192.168.0.72" # can also be `mail.mydomain.net`
+export SITE_SSL_PORT="993" # imap port dovecot
+
+##works: check if certificate will expire in two weeks 
+#2 weeks is 1209600 seconds
+#3 weeks is 1814400
+#12 weeks is 7257600
+#15 weeks is 9072000
+
+certcheck_2weeks=`openssl s_client -connect ${SITE_IP_URL}:${SITE_SSL_PORT} \
+  -servername ${SITE_URL} 2> /dev/null | openssl x509 -noout -checkend 1209600`
+
+####################################
+#notes: output can be
+#Certificate will not expire
+#Certificate will expire
+####################
+
+

What does the script that imports the certificates do:

+
    +
  1. Check if there are new certs in the /tmp/ssl folder.
  2. +
  3. Check with the ssl cert fingerprint if they differ from the current certificates.
  4. +
  5. If so it will copy the certs to the right places.
  6. +
  7. And restart postfix and dovecot.
  8. +
+

You can of course run the script by cron once a week or something. In that way you could automate cert renewal. If you do so it is probably wise to run an automated check on certificate expiry as well. Such a check could look something like this:

+
## code below will alert if certificate expires in less than two weeks
+## please adjust varables! 
+## make sure the mail -s command works! Test!
+
+export SITE_URL="mail.mydomain.net"
+export SITE_IP_URL="192.168.2.72" # can also be `mail.mydomain.net`
+export SITE_SSL_PORT="993" # imap port dovecot
+export ALERT_EMAIL_ADDR="bill@gates321boom.com"
+
+certcheck_2weeks=`openssl s_client -connect ${SITE_IP_URL}:${SITE_SSL_PORT} \
+  -servername ${SITE_URL} 2> /dev/null | openssl x509 -noout -checkend 1209600`
+
+####################################
+#notes: output can be
+#Certificate will not expire
+#Certificate will expire
+####################
+
+#echo "certcheck 2 weeks gives $certcheck_2weeks"
+
+##automated check you might run by cron or something
+## does tls/ssl certificate expire within two weeks?
+
+if [ "$certcheck_2weeks" = "Certificate will not expire" ]; then
+  echo "all is well, certwatch 2 weeks says $certcheck_2weeks"
+  else
+    echo "Cert seems to be expiring pretty soon, within two weeks: $certcheck_2weeks"
+    echo "we will send an alert email and log as well"
+    logger Certwatch: cert $SITE_URL will expire in two weeks
+    echo "Certwatch: cert $SITE_URL will expire in two weeks" | mail -s "cert $SITE_URL expires in two weeks " $ALERT_EMAIL_ADDR 
+fi
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/security/understanding-the-ports/index.html b/v10.0/config/security/understanding-the-ports/index.html new file mode 100644 index 00000000..39a63466 --- /dev/null +++ b/v10.0/config/security/understanding-the-ports/index.html @@ -0,0 +1,1647 @@ + + + + + + + + + + + + + + + + + + + + + + + Security | Understanding the Ports - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + +
+
+ + + + + + + +

Understanding the Ports

+ +

Quick Reference

+

Prefer Implicit TLS ports, they're more secure and if you use a Reverse Proxy, should be less hassle (although it's probably wiser to expose these ports directly to docker-mailserver).

+

Overview of Email Ports

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProtocolExplicit TLS1Implicit TLSPurpose
SMTP25N/ATransfer2
ESMTP5874653Submission
POP3110995Retrieval
IMAP4143993Retrieval
+
    +
  1. A connection may be secured over TLS when both ends support STARTTLS. On ports 110, 143 and 587, docker-mailserver will reject a connection that cannot be secured. Port 25 is required to support insecure connections.
  2. +
  3. Receives email, docker-mailserver additionally filters for spam and viruses. For submitting email to the server to be sent to third-parties, you should prefer the submission ports(465, 587) - which require authentication. Unless a relay host is configured(eg SendGrid), outgoing email will leave the server via port 25(thus outbound traffic must not be blocked by your provider or firewall).
  4. +
  5. A submission port since 2018 (RFC 8314). Previously a secure variant of port 25.
  6. +
+

What Ports Should I Use? (SMTP)

+

Best Practice - Ports (SMTP)

+
Flowchart - Mermaid.js source:

View in the Live Editor.

+
flowchart LR
+    subgraph your-server ["Your Server"]
+        in_25(25) --> server
+        in_465(465) --> server
+        server(("docker-mailserver<br/>hello@world.com"))
+        server --- out_25(25)
+        server --- out_465(465)
+    end
+
+    third-party("Third-party<br/>(sending you email)") ---|"Receive email for<br/>hello@world.com"| in_25
+
+    subgraph clients ["Clients (MUA)"]
+        mua-client(Thunderbird,<br/>Webmail,<br/>Mutt,<br/>etc)
+        mua-service(Backend software<br/>on another server)
+    end
+    clients ---|"Send email as<br/>hello@world.com"| in_465
+
+    out_25(25) -->|"Direct<br/>Delivery"| tin_25
+    out_465(465) --> relay("MTA<br/>Relay Server") --> tin_25(25)
+
+    subgraph third-party-server["Third-party Server"]
+        third-party-mta("MTA<br/>friend@example.com")
+        tin_25(25) --> third-party-mta
+    end
+
+
+
+

Inbound Traffic (On the left)

+
    +
  • Port 25: Think of this like a physical mailbox, it is open to receive email from anyone who wants to. docker-mailserver will actively filter email delivered on this port for spam or viruses and refuse mail from known bad sources. While you could also use this port internally to send email outbound without requiring authentication, you really should prefer the Submission ports(587, 465).
  • +
  • Port 465(and 587): This is the equivalent of a post office box where you would send email to be delivered on your behalf(docker-mailserver is that metaphorical post office, aka the MTA). Unlike port 25, these two ports are known as the Submission ports and require a valid email account on the server with a password to be able to send email to anyone outside of the server(an MTA you do not control, eg Outlook or Gmail). Prefer port 465 which provides Implicit TLS.
  • +
+

Outbound Traffic (On the Right)

+
    +
  • Port 25: Send the email directly to the given email address MTA as possible. Like your own docker-mailserver port 25, this is the standard port for receiving email on, thus email will almost always arrive to the final MTA on this port. Note that, there may be additional MTAs further in the chain, but this would be the public facing one representing that email address.
  • +
  • Port 465(and 587): SMTP Relays are a popular choice to hand-off delivery of email through. Services like SendGrid are useful for bulk email(marketing) or when your webhost or ISP are preventing you from using standard ports like port 25 to send out email(which can be abused by spammers).
  • +
+

docker-mailserver can serve as a relay too, but the difference between a DIY relay and a professional service is reputation, which is referenced by MTAs you're delivering to such as Outlook, Gmail or others(perhaps another docker-mailserver server!), when deciding if email should be marked as junked or potentially not delivered at all. As a service like SendGrid has a reputation to maintain, relay is restricted to registered users who must authenticate(even on port 25), they do not store email, merely forward it to another MTA which could be delivered on a different port like 25.

+

Explicit vs Implicit TLS

+

Explicit TLS (aka Opportunistic TLS) - Opt-in Encryption

+

Communication on these ports begin in cleartext, indicating support for STARTTLS. If both client and server support STARTTLS the connection will be secured over TLS, otherwise no encryption will be used.

+

Support for STARTTLS is not always implemented correctly, which can lead to leaking credentials(client sending too early) prior to a TLS connection being established. Third-parties such as some ISPs have also been known to intercept the STARTTLS exchange, modifying network traffic to prevent establishing a secure connection.

+

Due to these security concerns, RFC 8314 (Section 4.1) encourages you to prefer Implicit TLS ports where possible.

+

Implicit TLS - Enforced Encryption

+

Communication is always encrypted, avoiding the above mentioned issues with Explicit TLS.

+

You may know of these ports as SMTPS, POP3S, IMAPS, which indicate the protocol in combination with a TLS connection. However, Explicit TLS ports provide the same benefit when STARTTLS is successfully negotiated; Implicit TLS better communicates the improved security to all three protocols (SMTP/POP3/IMAP over Implicit TLS).

+

Additionally, referring to port 465 as SMTPS would be incorrect, as it is a submissions port requiring authentication to proceed via ESMTP, whereas ESMTPS has a different meaning(STARTTLS supported). Port 25 may lack Implicit TLS, but can be configured to be more secure between trusted parties via MTA-STS, STARTTLS Policy List, DNSSEC and DANE.

+

Security

+
+

Todo

+

This section should provide any related configuration advice, and probably expand on and link to resources about DANE, DNSSEC, MTA-STS and STARTTLS Policy list, with advice on how to configure/setup these added security layers.

+
+
+

Todo

+

A related section or page on ciphers used may be useful, although less important for users to be concerned about.

+
+

TLS connections on mail servers, compared to web browsers

+

Unlike with HTTP where a web browser client communicates directly with the server providing a website, a secure TLS connection as discussed below is not the equivalent safety that HTTPS provides when the transit of email (receiving or sending) is sent through third-parties, as the secure connection is only between two machines, any additional machines (MTAs) between the MUA and the MDA depends on them establishing secure connections between one another successfully.

+

Other machines that facilitate a connection that generally aren't taken into account can exist between a client and server, such as those where your connection passes through your ISP provider are capable of compromising a cleartext connection through interception.

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/setup.sh/index.html b/v10.0/config/setup.sh/index.html new file mode 100644 index 00000000..b96ae5a5 --- /dev/null +++ b/v10.0/config/setup.sh/index.html @@ -0,0 +1,1485 @@ + + + + + + + + + + + + + + + + + + + + + + + Your best friend setup.sh - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + + + +
+
+
+ + + + + + +
+
+
+ + + + + + + + +
+
+ + + + + + + +

Your Best Friend setup.sh

+ +

setup.sh is an administration script that helps with the most common tasks, including initial configuration. It is intented to be used from the host machine, not from within your running container.

+

The latest version of the script is included in the docker-mailserver repository. You may retrieve it at any time by running this command in your console:

+
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh
+chmod a+x ./setup.sh
+
+
+

Info

+

Make sure to get the setup.sh that comes with the release you're using. Look up the release and the git commit on which this release is based upon by selecting the appropriate tag on GitHub. This can done with the "Switch branches/tags" button on GitHub, choosing the right tag. This is done in order to rule out possible inconsistencies between versions.

+
+

Usage

+

Run ./setup.sh help and you'll get some usage information:

+
SETUP(1)
+
+NAME
+    setup.sh - docker-mailserver administration script
+
+SYNOPSIS
+    ./setup.sh [ OPTIONS... ] COMMAND [ help | ARGUMENTS... ]
+
+    COMMAND := { email | alias | quota | config | relay | debug } SUBCOMMAND
+
+DESCRIPTION
+    This is the main administration script that you use for all interactions with your
+    mail server. Setup, configuration and much more is done with this script.
+
+    Please note that the script executes most of the commands inside the container itself.
+    If the image was not found, this script will pull the :latest tag of
+    mailserver/docker-mailserver. This tag refers to the latest release,
+    see the tagging convention in the README under
+    https://github.com/docker-mailserver/docker-mailserver/blob/master/README.md
+
+    You will be able to see detailed information about the script you are invoking and
+    its arguments by appending help after your command. Currently, this
+    does not work with all scripts.
+
+VERSION
+    The current version of this script is backwards compatible with versions of
+    docker-mailserver after 8.0.1. In case that there is not a more recent release,
+    this script is currently only working with the :edge tag.
+
+    You can download the script for your release by substituting TAG from the
+    following URL, where TAG looks like 'vX.X.X':
+    https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/TAG/setup.sh
+
+OPTIONS
+    Config path, container or image adjustments
+        -i IMAGE_NAME
+            Provides the name of the docker-mailserver image. The default value is
+            docker.io/mailserver/docker-mailserver:latest
+
+        -c CONTAINER_NAME
+            Provides the name of the running container.
+
+        -p PATH
+            Provides the config folder path. The default is
+            /home/maxwell/Dokumente/github/docker-mailserver/config/
+
+    SELinux
+        -z
+            Allows container access to the bind mount content that is shared among
+            multiple containers on a SELinux-enabled host.
+
+        -Z
+            Allows container access to the bind mount content that is private and
+            unshared with other containers on a SELinux-enabled host.
+
+[SUB]COMMANDS
+    COMMAND email :=
+        ./setup.sh email add <EMAIL ADDRESS> [<PASSWORD>]
+        ./setup.sh email update <EMAIL ADDRESS> [<PASSWORD>]
+        ./setup.sh email del [ OPTIONS... ] <EMAIL ADDRESS> [ <EMAIL ADDRESS>... ]
+        ./setup.sh email restrict <add|del|list> <send|receive> [<EMAIL ADDRESS>]
+        ./setup.sh email list
+
+    COMMAND alias :=
+        ./setup.sh alias add <EMAIL ADDRESS> <RECIPIENT>
+        ./setup.sh alias del <EMAIL ADDRESS> <RECIPIENT>
+        ./setup.sh alias list
+
+    COMMAND quota :=
+        ./setup.sh quota set <EMAIL ADDRESS> [<QUOTA>]
+        ./setup.sh quota del <EMAIL ADDRESS>
+
+    COMMAND config :=
+        ./setup.sh config dkim [ ARGUMENTS... ]
+
+    COMMAND relay :=
+        ./setup.sh relay add-domain <DOMAIN> <HOST> [<PORT>]
+        ./setup.sh relay add-auth <DOMAIN> <USERNAME> [<PASSWORD>]
+        ./setup.sh relay exclude-domain <DOMAIN>
+
+    COMMAND debug :=
+        ./setup.sh debug fetchmail
+        ./setup.sh debug fail2ban [unban <IP>]
+        ./setup.sh debug show-mail-logs
+        ./setup.sh debug inspect
+        ./setup.sh debug login <COMMANDS>
+
+EXAMPLES
+    ./setup.sh email add test@domain.tld
+        Add the email account test@domain.tld. You will be prompted
+        to input a password afterwards since no password was supplied.
+
+    ./setup.sh config dkim keysize 2048 domain 'whoami.com,whoareyou.org'
+        Creates keys of length 2048 but in an LDAP setup where domains are not known to
+        Postfix by default, so you need to provide them yourself in a comma-separated list.
+
+    ./setup.sh config dkim help
+        This will provide you with a detailed explanation on how to use the 
+        config dkim command, showing what arguments can be passed and what they do.
+
+EXIT STATUS
+    Exit status is 0 if the command was successful. If there was an unexpected error, an error
+    message is shown describing the error. In case of an error, the script will exit with exit
+    status 1.
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/troubleshooting/debugging/index.html b/v10.0/config/troubleshooting/debugging/index.html new file mode 100644 index 00000000..46695d3d --- /dev/null +++ b/v10.0/config/troubleshooting/debugging/index.html @@ -0,0 +1,1475 @@ + + + + + + + + + + + + + + + + + + + + + + + Troubleshooting | Debugging - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+ +
+ + +
+
+ + + + + + + +

Debugging

+ +
+

Contributions Welcome!

+

Please contribute your solutions to help the community ❤

+
+

Enable Verbose Debugging Output

+

You may find it useful to enable the DMS_DEBUG environment variable.

+

Invalid Username or Password

+
    +
  1. +

    Shell into the container:

    +
    docker exec -it <my-container> bash
    +
    +
  2. +
  3. +

    Check log files in /var/log/mail could not find any mention of incorrect logins here neither in the dovecot logs.

    +
  4. +
  5. +

    Check the supervisors logs in /var/log/supervisor. You can find the logs for startup of fetchmail, postfix and others here - they might indicate problems during startup.

    +
  6. +
  7. +

    Make sure you set your hostname to mail or whatever you specified in your docker-compose.yml file or else your FQDN will be wrong.

    +
  8. +
+

Installation Errors

+

During setup, if you get errors trying to edit files inside of the container, you likely need to install vi:

+
sudo su
+docker exec -it <my-container> apt-get install -y vim
+
+

Testing Connection

+

I spent HOURS trying to debug "Connection Refused" and "Connection closed by foreign host" errors when trying to use telnet to troubleshoot my connection. I was also trying to connect from my email client (macOS mail) around the same time. Telnet had also worked earlier, so I was extremely confused as to why it suddenly stopped working. I stumbled upon fail2ban.log in my container. In short, when trying to get my macOS client working, I exceeded the number of failed login attempts and fail2ban put dovecot and postfix in jail! I got around it by whitelisting my ipaddresses (my ec2 instance and my local computer)

+
sudo su
+docker exec -ti mail bash
+cd /var/log
+cat fail2ban.log | grep dovecot
+
+# Whitelist IP addresses:
+fail2ban-client set dovecot addignoreip <server ip>  # Server
+fail2ban-client set postfix addignoreip <server ip>
+fail2ban-client set dovecot addignoreip <client ip>  # Client
+fail2ban-client set postfix addignoreip <client ip>
+
+# This will delete the jails entirely - nuclear option
+fail2ban-client stop dovecot
+fail2ban-client stop postfix
+
+

Sent email is never received

+

Some hosting provides have a stealth block on port 25. Make sure to check with your hosting provider that traffic on port 25 is allowed

+

Common hosting providers known to have this issue:

+ + + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/user-management/accounts/index.html b/v10.0/config/user-management/accounts/index.html new file mode 100644 index 00000000..5b9ecc68 --- /dev/null +++ b/v10.0/config/user-management/accounts/index.html @@ -0,0 +1,1426 @@ + + + + + + + + + + + + + + + + + + + + + + + User Management | Accounts - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + + + +
+
+
+ + + + + + +
+
+
+ + + + + + + + +
+
+ + + + + + + +

Accounts

+ +

Adding a New Account

+

Users (email accounts) are managed in /tmp/docker-mailserver/postfix-accounts.cf. The best way to manage accounts is to use the reliable setup.sh script. Or you may directly add the full email address and its encrypted password, separated by a pipe:

+
user1@domain.tld|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1
+user2@otherdomain.tld|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1
+
+

In the example above, we've added 2 mail accounts for 2 different domains. Consequently, the mail server will automatically be configured for multi-domains. Therefore, to generate a new mail account data, directly from your docker host, you could for example run the following:

+
docker run --rm \
+  -e MAIL_USER=user1@domain.tld \
+  -e MAIL_PASS=mypassword \
+  -it mailserver/docker-mailserver:latest \
+  /bin/sh -c 'echo "$MAIL_USER|$(doveadm pw -s SHA512-CRYPT -u $MAIL_USER -p $MAIL_PASS)"' >> config/postfix-accounts.cf
+
+

You will then be asked for a password, and be given back the data for a new account entry, as text. To actually add this new account, just copy all the output text in config/postfix-accounts.cf file of your running container.

+
+

Note

+

doveadm pw command lets you choose between several encryption schemes for the password.

+

Use doveadm pw -l to get a list of the currently supported encryption schemes.

+
+
+

Note

+

Changes to the accounts list require a restart of the container, using supervisord. See #552.

+
+
+

Notes

+
    +
  • imap-quota is enabled and allow clients to query their mailbox usage.
  • +
  • When the mailbox is deleted, the quota directive is deleted as well.
  • +
  • Dovecot quotas support LDAP, but it's not implemented (PR are welcome!).
  • +
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/config/user-management/aliases/index.html b/v10.0/config/user-management/aliases/index.html new file mode 100644 index 00000000..6192e8f7 --- /dev/null +++ b/v10.0/config/user-management/aliases/index.html @@ -0,0 +1,1406 @@ + + + + + + + + + + + + + + + + + + + + + + + User Management | Aliases - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Aliases

+ +

Please read the Postfix documentation on virtual aliases first.

+

You can use setup.sh instead of creating and editing files manually. Aliases are managed in /tmp/docker-mailserver/postfix-virtual.cf. An alias is a full email address that will either be:

+
    +
  • delivered to an existing account registered in /tmp/docker-mailserver/postfix-accounts.cf
  • +
  • redirected to one or more other email addresses
  • +
+

Alias and target are space separated. An example on a server with domain.tld as its domain:

+
# Alias delivered to an existing account
+alias1@domain.tld user1@domain.tld
+
+# Alias forwarded to an external email address
+alias2@domain.tld external@gmail.com
+
+

Configuring RegExp Aliases

+

Additional regexp aliases can be configured by placing them into config/postfix-regexp.cf. The regexp aliases get evaluated after the virtual aliases (/tmp/docker-mailserver/postfix-virtual.cf). For example, the following config/postfix-regexp.cf causes all email to "test" users to be delivered to qa@example.com:

+
/^test[0-9][0-9]*@example.com/ qa@example.com
+
+

Address Tags (Extension Delimiters) an Alternative to Aliases

+

Postfix supports so-called address tags, in the form of plus (+) tags - i.e. address+tag@example.com will end up at address@example.com. This is configured by default and the (configurable !) separator is set to +. For more info, see How to use Address Tagging (user+tag@example.com) with Postfix and the official documentation.

+
+

Note

+

If you do decide to change the configurable separator, you must add the same line to both config/postfix-main.cf and config/dovecot.cf, because Dovecot is acting as the delivery agent. For example, to switch to -, add:

+
+
recipient_delimiter = -
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/contributing/coding-style/index.html b/v10.0/contributing/coding-style/index.html new file mode 100644 index 00000000..ac515eb4 --- /dev/null +++ b/v10.0/contributing/coding-style/index.html @@ -0,0 +1,1609 @@ + + + + + + + + + + + + + + + + + + + + + + + Contributing | Coding Style - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Coding Style

+ +

Bash and Shell

+

When refactoring, writing or altering scripts, that is Shell and bash scripts, in any way, adhere to these rules:

+
    +
  1. Adjust your style of coding to the style that is already present! Even if you do not like it, this is due to consistency. There was a lot of work involved in making all scripts consistent.
  2. +
  3. Use shellcheck to check your scripts! Your contributions are checked by GitHub Actions too, so you will need to do this. You can lint your work with make lint to check against all targets.
  4. +
  5. Use the provided .editorconfig file.
  6. +
  7. Use /bin/bash instead of /bin/sh. Adjust the style accordingly.
  8. +
  9. setup.sh provides a good starting point to look for.
  10. +
  11. When appropriate, use the set builtin. We recommend set -euEo pipefail or set -uE.
  12. +
+

Styling rules

+

If-Else-Statements

+
# when using braces, use double braces
+# remember you do not need "" when using [[ ]]
+if [[ <CONDITION1> ]] && [[ -f ${FILE} ]]
+then
+  <CODE TO RUN>
+# when running commands, you don't need braces
+elif <COMMAND TO RUN>
+  <CODE TO TUN>
+else
+  <CODE TO TUN>
+fi
+
+# equality checks with numbers are done
+# with -eq/-ne/-lt/-ge, not != or ==
+if [[ ${VAR} -ne 42 ]] || [[ ${SOME_VAR} -eq 6 ]]
+then
+  <CODE TO RUN>
+fi
+
+

Variables & Braces

+
+

Attention

+

Variables are always uppercase. We always use braces.

+
+

If you forgot this and want to change it later, you can use this link. The used regex is \$([^{("\\'\/])([a-zA-Z0-9_]*)([^}\/ \t'"\n.\]:(=\\-]*), where you should in practice be able to replace all variable occurrences without braces with occurrences with braces.

+
# good
+local VAR="good"
+local NEW="${VAR}"
+
+# bad -> CI will fail
+var="bad"
+new=$var
+
+

Loops

+

Like if-else, loops look like this

+
for / while <LOOP CONDITION>
+do
+  <CODE TO RUN>
+done
+
+

Functions

+

It's always nice to see the use of functions as it also provides a clear structure. If scripts are small, this is unnecessary, but if they become larger, please consider using functions. When doing so, provide function _main.

+
function _<name_underscored_and_lowercase>
+{
+  <CODE TO RUN>
+
+  # variables that can be local should be local
+  local <LOCAL_VARIABLE_NAME>
+}
+
+

Error Tracing

+

A construct to trace error in your scripts looks like this. Remember: Remove set -x in the end. This is for debugging purposes only.

+
set -xeuEo pipefail
+trap '__log_err ${FUNCNAME[0]:-"?"} ${BASH_COMMAND:-"?"} ${LINENO:-"?"} ${?:-"?"}' ERR
+
+SCRIPT='name_of_this_script.sh'
+
+function __log_err
+{
+  printf "\n––– \e[1m\e[31mUNCHECKED ERROR\e[0m\n%s\n%s\n%s\n%s\n\n" \
+    "  – script    = ${SCRIPT:-${0}}" \
+    "  – function  = ${1} / ${2}" \
+    "  – line      = ${3}" \
+    "  – exit code = ${4}" 1>&2
+
+  <CODE TO RUN AFTERWARDS>
+}
+
+

Comments, Descriptiveness & An Example

+

Comments should only describe non-obvious matters. Comments should start lowercase when they aren't sentences. Make the code self-descriptive by using meaningful names! Make comments not longer than approximately 80 columns, then wrap the line.

+

A positive example, which is taken from start-mailserver.sh, would be

+
function _setup_postfix_aliases
+{
+  _notify 'task' 'Setting up Postfix Aliases'
+
+  : >/etc/postfix/virtual
+  : >/etc/postfix/regexp
+
+  if [[ -f /tmp/docker-mailserver/postfix-virtual.cf ]]
+  then
+    # fixing old virtual user file
+    if grep -q ",$" /tmp/docker-mailserver/postfix-virtual.cf
+    then
+      sed -i -e "s/, /,/g" -e "s/,$//g" /tmp/docker-mailserver/postfix-virtual.cf
+    fi
+
+    cp -f /tmp/docker-mailserver/postfix-virtual.cf /etc/postfix/virtual
+
+    # the `to` is important, don't delete it
+    # shellcheck disable=SC2034
+    while read -r FROM TO
+    do
+      # Setting variables for better readability
+      UNAME=$(echo "${FROM}" | cut -d @ -f1)
+      DOMAIN=$(echo "${FROM}" | cut -d @ -f2)
+
+      # if they are equal it means the line looks like: "user1     other@domain.tld"
+      [[ "${UNAME}" != "${DOMAIN}" ]] && echo "${DOMAIN}" >> /tmp/vhost.tmp
+    done < <(grep -v "^\s*$\|^\s*\#" /tmp/docker-mailserver/postfix-virtual.cf || true)
+  else
+    _notify 'inf' "Warning 'config/postfix-virtual.cf' is not provided. No mail alias/forward created."
+  fi
+
+  ...
+}
+
+

YAML

+

When formatting YAML files, use Prettier, an opinionated formatter. There are many plugins for IDEs around.

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/contributing/documentation/index.html b/v10.0/contributing/documentation/index.html new file mode 100644 index 00000000..1c53c38f --- /dev/null +++ b/v10.0/contributing/documentation/index.html @@ -0,0 +1,1292 @@ + + + + + + + + + + + + + + + + + + + + + + + Contributing | Documentation - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+ + + + + + + +

Documentation

+ +
+

Todo

+

This section should provide a detailed step by step guide on how to contribute to documentation

+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/contributing/issues-and-pull-requests/index.html b/v10.0/contributing/issues-and-pull-requests/index.html new file mode 100644 index 00000000..012e958d --- /dev/null +++ b/v10.0/contributing/issues-and-pull-requests/index.html @@ -0,0 +1,1439 @@ + + + + + + + + + + + + + + + + + + + + + + + Contributing | Issues and Pull Requests - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Issues and Pull Requests

+ +

This project is Open Source. That means that you can contribute on enhancements, bug fixing or improving the documentation.

+

Opening an Issue

+
+

Attention

+

Before opening an issue, read the README carefully, study the documentation, the Postfix/Dovecot documentation and your search engine you trust. The issue tracker is not meant to be used for unrelated questions!

+
+

When opening an issue, please provide details use case to let the community reproduce your problem. Please start the mail server with env DMS_DEBUG=1 and paste the output into the issue.

+
+

Attention

+

Use the issue templates to provide the necessary information. Issues which do not use these templates are not worked on and closed.

+
+

By raising issues, I agree to these terms and I understand, that the rules set for the issue tracker will help both maintainers as well as everyone to find a solution.

+

Maintainers take the time to improve on this project and help by solving issues together. It is therefore expected from others to make an effort and comply with the rules.

+

Pull Requests

+

Submit a Pull-Request

+
+

Motivation

+

You want to add a feature? Feel free to start creating an issue explaining what you want to do and how you're thinking doing it. Other users may have the same need and collaboration may lead to better results.

+
+

The development workflow is the following:

+
    +
  1. Fork the project and clone your fork
  2. +
  3. Create a new branch to work on
  4. +
  5. Run git submodule update --init --recursive
  6. +
  7. Write the code that is needed :D
  8. +
  9. Add integration tests if necessary
  10. +
  11. Get the linters with make install_linters and install jq with the package manager of your OS
  12. +
  13. Use make clean all to build image locally and run tests (note that tests work on Linux only)
  14. +
  15. Document your improvements if necessary (e.g. if you introduced new environment variables, describe those in the ENV documentation)
  16. +
  17. Commit and sign your commit, push and create a pull-request to merge into master. Please use the pull-request template to provide a minimum of contextual information and make sure to meet the requirements of the checklist.
  18. +
  19. Pull requests are automatically tested against the CI and will be reviewed when tests pass
  20. +
  21. When your changes are validated, your branch is merged
  22. +
  23. CI builds the new :edge image immediately and your changes will be includes in the next version release.
  24. +
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/contributing/tests/index.html b/v10.0/contributing/tests/index.html new file mode 100644 index 00000000..e3cc38df --- /dev/null +++ b/v10.0/contributing/tests/index.html @@ -0,0 +1,1306 @@ + + + + + + + + + + + + + + + + + + + + + + + Contributing | Tests - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+ + + + + + + +

Tests

+ +
+

Todo

+

This section should provide a detailed step by step guide on how to write tests

+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/examples/tutorials/basic-installation/index.html b/v10.0/examples/tutorials/basic-installation/index.html new file mode 100644 index 00000000..38e8b35d --- /dev/null +++ b/v10.0/examples/tutorials/basic-installation/index.html @@ -0,0 +1,1514 @@ + + + + + + + + + + + + + + + + + + + + + + + Tutorials | Basic Installation - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Basic Installation

+ +

Building a Simple Mailserver

+
+

Warning

+

Adding the docker network's gateway to the list of trusted hosts, e.g. using the network or connected-networks option, can create an open relay, for instance if IPv6 is enabled on the host machine but not in Docker.

+
+

We are going to use this docker based mailserver:

+
    +
  • +

    First create a directory for the mailserver and get the setup script:

    +
    mkdir -p /var/ds/mail.example.org
    +cd /var/ds/mail.example.org/
    +
    +curl -o setup.sh \
    +    https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh
    +chmod a+x ./setup.sh
    +
    +
  • +
  • +

    Create the file docker-compose.yml with a content like this:

    +
    +

    Example

    +
    version: '2'
    +
    +services:
    +mail:
    +    image: mailserver/docker-mailserver:latest
    +    hostname: mail
    +    domainname: example.org
    +    container_name: mail
    +    ports:
    +    - "25:25"
    +    - "587:587"
    +    - "465:465"
    +    volumes:
    +    - ./data/:/var/mail/
    +    - ./state/:/var/mail-state/
    +    - ./config/:/tmp/docker-mailserver/
    +    - /var/ds/wsproxy/letsencrypt/:/etc/letsencrypt/
    +    environment:
    +    - PERMIT_DOCKER=network
    +    - SSL_TYPE=letsencrypt
    +    - ONE_DIR=1
    +    - DMS_DEBUG=1
    +    - SPOOF_PROTECTION=0
    +    - REPORT_RECIPIENT=1
    +    - ENABLE_SPAMASSASSIN=0
    +    - ENABLE_CLAMAV=0
    +    - ENABLE_FAIL2BAN=1
    +    - ENABLE_POSTGREY=0
    +    cap_add:
    +    - NET_ADMIN
    +    - SYS_PTRACE
    +
    +
    +

    For more details about the environment variables that can be used, and their meaning and possible values, check also these:

    + +

    Make sure to set the proper domainname that you will use for the emails. We forward only SMTP ports (not POP3 and IMAP) because we are not interested in accessing the mailserver directly (from a client). We also use these settings:

    +
      +
    • PERMIT_DOCKER=network because we want to send emails from other docker containers.
    • +
    • SSL_TYPE=letsencrypt because we will manage SSL certificates with letsencrypt.
    • +
    +
  • +
  • +

    We need to open ports 25, 587 and 465 on the firewall:

    +
    ufw allow 25
    +ufw allow 587
    +ufw allow 465
    +
    +

    On your server you may have to do it differently.

    +
  • +
  • +

    Pull the docker image: docker pull mailserver/docker-mailserver:latest

    +
  • +
  • +

    Now generate the DKIM keys with ./setup.sh config dkim and copy the content of the file config/opendkim/keys/domain.tld/mail.txt on the domain zone configuration at the DNS server. I use bind9 for managing my domains, so I just paste it on example.org.db:

    +
    mail._domainkey IN      TXT     ( "v=DKIM1; h=sha256; k=rsa; "
    +        "p=MIIBIjANBgkqhkiG9w0BAQEFACAQ8AMIIBCgKCAQEAaH5KuPYPSF3Ppkt466BDMAFGOA4mgqn4oPjZ5BbFlYA9l5jU3bgzRj3l6/Q1n5a9lQs5fNZ7A/HtY0aMvs3nGE4oi+LTejt1jblMhV/OfJyRCunQBIGp0s8G9kIUBzyKJpDayk2+KJSJt/lxL9Iiy0DE5hIv62ZPP6AaTdHBAsJosLFeAzuLFHQ6USyQRojefqFQtgYqWQ2JiZQ3"
    +        "iqq3bD/BVlwKRp5gH6TEYEmx8EBJUuDxrJhkWRUk2VDl1fqhVBy8A9O7Ah+85nMrlOHIFsTaYo9o6+cDJ6t1i6G1gu+bZD0d3/3bqGLPBQV9LyEL1Rona5V7TJBGg099NQkTz1IwIDAQAB" )  ; ----- DKIM key mail for example.org
    +
    +
  • +
  • +

    Add these configurations as well on the same file on the DNS server:

    +
    mail      IN  A   10.11.12.13
    +
    +; mailservers for example.org
    +    3600  IN  MX  1  mail.example.org.
    +
    +; Add SPF record
    +          IN TXT "v=spf1 mx ~all"
    +
    +

    Then don't forget to change the serial number and to restart the service.

    +
  • +
  • +

    Get an SSL certificate from letsencrypt. I use wsproxy for managing SSL letsencrypt certificates of my domains:

    +
    cd /var/ds/wsproxy
    +ds domains-add mail mail.example.org
    +ds get-ssl-cert myemail@gmail.com mail.example.org --test
    +ds get-ssl-cert myemail@gmail.com mail.example.org
    +
    +

    Now the certificates will be available on /var/ds/wsproxy/letsencrypt/live/mail.example.org.

    +
  • +
  • +

    Start the mailserver and check for any errors:

    +
    apt install docker-compose
    +docker-compose up mail
    +
    +
  • +
  • +

    Create email accounts and aliases with SPOOF_PROTECTION=0:

    +
    ./setup.sh email add admin@example.org passwd123
    +./setup.sh email add info@example.org passwd123
    +./setup.sh alias add admin@example.org myemail@gmail.com
    +./setup.sh alias add info@example.org myemail@gmail.com
    +./setup.sh email list
    +./setup.sh alias list
    +
    +

    Aliases make sure that any email that comes to these accounts is forwarded to my real email address, so that I don't need to use POP3/IMAP in order to get these messages. Also no anti-spam and anti-virus software is needed, making the mailserver lighter.

    +
  • +
  • +

    Or create email accounts and aliases with SPOOF_PROTECTION=1:

    +
    ./setup.sh email add admin.gmail@example.org passwd123
    +./setup.sh email add info.gmail@example.org passwd123
    +./setup.sh alias add admin@example.org admin.gmail@example.org
    +./setup.sh alias add info@example.org info.gmail@example.org
    +./setup.sh alias add admin.gmail@example.org myemail@gmail.com
    +./setup.sh alias add info.gmail@example.org myemail@gmail.com
    +./setup.sh email list
    +./setup.sh alias list
    +
    +

    This extra step is required to avoid the 553 5.7.1 Sender address rejected: not owned by user error (the account used for setting up gmail is admin.gmail@example.org and info.gmail@example.org )

    +
  • +
  • +

    Send some test emails to these addresses and make other tests. Then stop the container with ctrl+c and start it again as a daemon: docker-compose up -d mail.

    +
  • +
  • +

    Now save on Moodle configuration the SMTP settings and test by trying to send some messages to other users:

    +
      +
    • SMTP hosts: mail.example.org:465
    • +
    • SMTP security: SSL
    • +
    • SMTP username: info@example.org
    • +
    • SMTP password: passwd123
    • +
    +
  • +
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/examples/tutorials/mailserver-behind-proxy/index.html b/v10.0/examples/tutorials/mailserver-behind-proxy/index.html new file mode 100644 index 00000000..b2b53ad4 --- /dev/null +++ b/v10.0/examples/tutorials/mailserver-behind-proxy/index.html @@ -0,0 +1,1517 @@ + + + + + + + + + + + + + + + + + + + + + + + Tutorials | Mailserver behind Proxy - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + +
+
+ + + + + + + +

Mailserver behind Proxy

+ +

Using docker-mailserver behind a Proxy

+

Information

+

If you are hiding your container behind a proxy service you might have discovered that the proxied requests from now on contain the proxy IP as the request origin. Whilst this behavior is technical correct it produces certain problems on the containers behind the proxy as they cannot distinguish the real origin of the requests anymore.

+

To solve this problem on TCP connections we can make use of the proxy protocol. Compared to other workarounds that exist (X-Forwarded-For which only works for HTTP requests or Tproxy that requires you to recompile your kernel) the proxy protocol:

+
    +
  • It is protocol agnostic (can work with any layer 7 protocols, even when encrypted).
  • +
  • It does not require any infrastructure changes.
  • +
  • NAT-ing firewalls have no impact it.
  • +
  • It is scalable.
  • +
+

There is only one condition: both endpoints of the connection MUST be compatible with proxy protocol.

+

Luckily dovecot and postfix are both Proxy-Protocol ready softwares so it depends only on your used reverse-proxy / loadbalancer.

+

Configuration of the used Proxy Software

+

The configuration depends on the used proxy system. I will provide the configuration examples of traefik v2 using IMAP and SMTP with implicit TLS.

+

Feel free to add your configuration if you achived the same goal using different proxy software below:

+
Traefik v2

Truncated configuration of traefik itself:

+
version: '3.7'
+services:
+  reverse-proxy:
+    image: traefik:v2.4
+    container_name: docker-traefik
+    restart: always
+    command:
+      - "--providers.docker"
+      - "--providers.docker.exposedbydefault=false"
+      - "--providers.docker.network=proxy"
+      - "--entrypoints.web.address=:80"
+      - "--entryPoints.websecure.address=:443"
+      - "--entryPoints.smtp.address=:25"
+      - "--entryPoints.smtp-ssl.address=:465"
+      - "--entryPoints.imap-ssl.address=:993"
+      - "--entryPoints.sieve.address=:4190"
+    ports:
+      - "25:25"
+      - "465:465"
+      - "993:993"
+      - "4190:4190"
+[...]
+
+

Truncated list of neccessary labels on the mailserver container:

+
version: '2'
+services:
+  mail:
+    image: mailserver/docker-mailserver:release-v7.2.0
+    restart: always
+    networks:
+      - proxy
+    labels:
+      - "traefik.enable=true"
+      - "traefik.tcp.routers.smtp.rule=HostSNI(`*`)"
+      - "traefik.tcp.routers.smtp.entrypoints=smtp"
+      - "traefik.tcp.routers.smtp.service=smtp"
+      - "traefik.tcp.services.smtp.loadbalancer.server.port=25"
+      - "traefik.tcp.services.smtp.loadbalancer.proxyProtocol.version=1"
+      - "traefik.tcp.routers.smtp-ssl.rule=HostSNI(`*`)"
+      - "traefik.tcp.routers.smtp-ssl.entrypoints=smtp-ssl"
+      - "traefik.tcp.routers.smtp-ssl.service=smtp-ssl"
+      - "traefik.tcp.services.smtp-ssl.loadbalancer.server.port=465"
+      - "traefik.tcp.services.smtp-ssl.loadbalancer.proxyProtocol.version=1"
+      - "traefik.tcp.routers.imap-ssl.rule=HostSNI(`*`)"
+      - "traefik.tcp.routers.imap-ssl.entrypoints=imap-ssl"
+      - "traefik.tcp.routers.imap-ssl.service=imap-ssl"
+      - "traefik.tcp.services.imap-ssl.loadbalancer.server.port=10993"
+      - "traefik.tcp.services.imap-ssl.loadbalancer.proxyProtocol.version=2"
+      - "traefik.tcp.routers.sieve.rule=HostSNI(`*`)"
+      - "traefik.tcp.routers.sieve.entrypoints=sieve"
+      - "traefik.tcp.routers.sieve.service=sieve"
+      - "traefik.tcp.services.sieve.loadbalancer.server.port=4190"
+[...]
+
+

Keep in mind that it is neccessary to use port 10993 here. More information below at dovecot configuration.

+
+

Configuration of the Backend (dovecot and postfix)

+

The following changes can be achived completely by adding the content to the appropriate files by using the projects function to overwrite config files.

+

Changes for postfix can be applied by adding the following content to config/postfix-main.cf:

+
postscreen_upstream_proxy_protocol = haproxy
+
+

and to config/postfix-master.cf:

+
submission/inet/smtpd_upstream_proxy_protocol=haproxy
+smtps/inet/smtpd_upstream_proxy_protocol=haproxy
+
+

Changes for dovecot can be applied by adding the following content to config/dovecot.cf:

+
haproxy_trusted_networks = <your-proxy-ip>, <optional-cidr-notation>
+haproxy_timeout = 3 secs
+service imap-login {
+  inet_listener imaps {
+    haproxy = yes
+    ssl = yes
+    port = 10993
+  }
+}
+
+
+

Note

+

Port 10993 is used here to avoid conflicts with internal systems like postscreen and amavis as they will exchange messages on the default port and obviously have a different origin then compared to the proxy.

+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/examples/uses-cases/forward-only-mailserver-with-ldap-authentication/index.html b/v10.0/examples/uses-cases/forward-only-mailserver-with-ldap-authentication/index.html new file mode 100644 index 00000000..ad7c670d --- /dev/null +++ b/v10.0/examples/uses-cases/forward-only-mailserver-with-ldap-authentication/index.html @@ -0,0 +1,1467 @@ + + + + + + + + + + + + + + + + + + + + + + + Use Cases | Forward-Only Mailserver with LDAP - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Forward-Only Mailserver with LDAP

+ +

Building a Forward-Only Mailserver

+

A forward-only mailserver does not have any local mailboxes. Instead, it has only aliases that forward emails to external email accounts (for example to a gmail account). You can also send email from the localhost (the computer where the mailserver is installed), using as sender any of the alias addresses.

+

The important settings for this setup (on mailserver.env) are these:

+
PERMIT_DOCKER=host
+ENABLE_POP3=
+ENABLE_CLAMAV=0
+SMTP_ONLY=1
+ENABLE_SPAMASSASSIN=0
+ENABLE_FETCHMAIL=0
+
+

Since there are no local mailboxes, we use SMTP_ONLY=1 to disable dovecot. We disable as well the other services that are related to local mailboxes (POP3, ClamAV, SpamAssassin, etc.)

+

We can create aliases with ./setup.sh, like this:

+
./setup.sh alias add <alias-address> <external-email-account>
+
+

Authenticating with LDAP

+

If you want to send emails from outside the mailserver you have to authenticate somehow (with a username and password). One way of doing it is described in this discussion. However if there are many user accounts, it is better to use authentication with LDAP. The settings for this on mailserver.env are:

+
ENABLE_LDAP=1
+LDAP_START_TLS=yes
+LDAP_SERVER_HOST=ldap.example.org
+LDAP_SEARCH_BASE=ou=users,dc=example,dc=org
+LDAP_BIND_DN=cn=mailserver,dc=example,dc=org
+LDAP_BIND_PW=pass1234
+
+ENABLE_SASLAUTHD=1
+SASLAUTHD_MECHANISMS=ldap
+SASLAUTHD_LDAP_SERVER=ldap.example.org
+SASLAUTHD_LDAP_START_TLS=yes
+SASLAUTHD_LDAP_BIND_DN=cn=mailserver,dc=example,dc=org
+SASLAUTHD_LDAP_PASSWORD=pass1234
+SASLAUTHD_LDAP_SEARCH_BASE=ou=users,dc=example,dc=org
+SASLAUTHD_LDAP_FILTER=(&(uid=%U)(objectClass=inetOrgPerson))
+
+

My LDAP data structure is very basic, containing only the username, password, and the external email address where to forward emails for this user. An entry looks like this

+
add uid=username,ou=users,dc=example,dc=org
+uid: username
+objectClass: inetOrgPerson
+sn: username
+cn: username
+userPassword: {SSHA}abcdefghi123456789
+email: real-email-address@external-domain.com
+
+

This structure is different from what is expected/assumed from the configuration scripts of the mailserver, so it doesn't work just by using the LDAP_QUERY_FILTER_... settings. Instead, I had to do custom configuration. I created the script config/user-patches.sh, with a content like this:

+
#!/bin/bash
+
+rm -f /etc/postfix/{ldap-groups.cf,ldap-domains.cf}
+
+postconf \
+    "virtual_mailbox_domains = /etc/postfix/vhost" \
+    "virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf texthash:/etc/postfix/virtual" \
+    "smtpd_sender_login_maps = ldap:/etc/postfix/ldap-users.cf"
+
+sed -i /etc/postfix/ldap-users.cf \
+    -e '/query_filter/d' \
+    -e '/result_attribute/d' \
+    -e '/result_format/d'
+cat <<EOF >> /etc/postfix/ldap-users.cf
+query_filter = (uid=%u)
+result_attribute = uid
+result_format = %s@example.org
+EOF
+
+sed -i /etc/postfix/ldap-aliases.cf \
+    -e '/domain/d' \
+    -e '/query_filter/d' \
+    -e '/result_attribute/d'
+cat <<EOF >> /etc/postfix/ldap-aliases.cf
+domain = example.org
+query_filter = (uid=%u)
+result_attribute = mail
+EOF
+
+postfix reload
+
+

You see that besides query_filter, I had to customize as well result_attribute and result_format.

+
+

Sealso

+

For more details about using LDAP see: LDAP managed mail server with Postfix and Dovecot for multiple domains

+
+
+

Note

+

Another solution that serves as a forward-only mailserver is this: https://gitlab.com/docker-scripts/postfix

+
+
+

Tip

+

One user reports only having success if ENABLE_LDAP=0 was set.

+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/faq/index.html b/v10.0/faq/index.html new file mode 100644 index 00000000..259c78fc --- /dev/null +++ b/v10.0/faq/index.html @@ -0,0 +1,2004 @@ + + + + + + + + + + + + + + + + + + + + + + + FAQ - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

FAQ

+ +

What kind of database are you using?

+

None! No database is required. Filesystem is the database.
+This image is based on config files that can be persisted using Docker volumes, and as such versioned, backed up and so forth.

+

Where are emails stored?

+

Mails are stored in /var/mail/${domain}/${username}. Since v9.0.0 it is possible to add custom user_attributes for each accounts to have a different mailbox configuration (See #1792).

+
+

Warning

+

You should use a data volume container for /var/mail to persist data. Otherwise, your data may be lost.

+
+

How to alter the running mailserver instance without relaunching the container?

+

docker-mailserver aggregates multiple "sub-services", such as Postfix, Dovecot, Fail2ban, SpamAssassin, etc. In many cases, one may edit a sub-service's config and reload that very sub-service, without stopping and relaunching the whole mail server.

+

In order to do so, you'll probably want to push your config updates to your server through a Docker volume, then restart the sub-service to apply your changes, using supervisorctl. For instance, after editing fail2ban's config: supervisorctl restart fail2ban.

+

See supervisorctl's documentation.

+
+

Tip

+

To add, update or delete an email account; there is no need to restart postfix / dovecot service inside the container after using setup.sh script.

+

For more information, see #1639.

+
+

How can I sync container with host date/time? Timezone?

+

Share the host's /etc/localtime with the docker-mailserver container, using a Docker volume:

+
volumes:
+  - /etc/localtime:/etc/localtime:ro
+
+
+

Optional

+

Add one line to .env or env-mailserver to set timetzone for container, for example:

+
TZ=Europe/Berlin
+
+

Check here for the tz name list

+
+

What is the file format?

+

All files are using the Unix format with LF line endings.

+

Please do not use CRLF.

+

What about backups?

+

Assuming that you use docker-compose and data volumes, you can backup the configuration, emails and logs like this:

+
# create backup
+docker run --rm -ti \
+  -v maildata:/var/mail \
+  -v mailstate:/var/mail-state \
+  -v maillogs:/var/logs/mail \
+  -v "$PWD/config":/tmp/docker-mailserver \
+  -v /backup/mail:/backup \
+  alpine:latest \
+  tar czf "/backup/mail-$(date +%F).tar.gz" /var/mail /var/mail-state /var/logs/mail /tmp/docker-mailserver
+
+# delete backups older than 30 days
+find /backup/mail -type f -mtime +30 -delete
+
+

What about mail-state folder?

+

This folder consolidates all data generated by the server itself to persist when you upgrade. +Example of data folder persisted: lib-amavis, lib-clamav, lib-fail2ban, lib-postfix, lib-postgrey, lib-spamassasin, lib-spamassassin, spool-postfix, ...

+

How can I configure my email client?

+

Login are full email address (user@domain.com).

+
# imap
+username:           <user1@domain.tld>
+password:           <mypassword>
+server:             <mail.domain.tld>
+imap port:          143 or 993 with ssl (recommended)
+imap path prefix:   INBOX
+
+# smtp
+smtp port:          25 or 587 with ssl (recommended)
+username:           <user1@domain.tld>
+password:           <mypassword>
+
+

Please use STARTTLS.

+

How can I manage my custom SpamAssassin rules?

+

Antispam rules are managed in config/spamassassin-rules.cf.

+

What are acceptable SA_SPAM_SUBJECT values?

+

For no subject set SA_SPAM_SUBJECT=undef.

+

For a trailing white-space subject one can define the whole variable with quotes in docker-compose.yml:

+
environment:
+  - "SA_SPAM_SUBJECT=[SPAM] "
+
+

Can I use naked/bare domains (no host name)?

+

Yes, but not without some configuration changes. Normally it is assumed that docker-mailserver runs on a host with a name, so the fully qualified host name might be mail.example.com with the domain example.com. The MX records point to mail.example.com.

+

To use a bare domain where the host name is example.com and the domain is also example.com, change mydestination:

+
    +
  • From: mydestination = $myhostname, localhost.$mydomain, localhost
  • +
  • To: mydestination = localhost.$mydomain, localhost
  • +
+

Add the latter line to config/postfix-main.cf. That should work. Without that change there will be warnings in the logs like:

+
warning: do not list domain example.com in BOTH mydestination and virtual_mailbox_domains
+
+

Plus of course mail delivery fails.

+

Why are SpamAssassin x-headers not inserted into my sample.domain.com subdomain emails?

+

In the default setup, amavis only applies SpamAssassin x-headers into domains matching the template listed in the config file (05-domain_id in the amavis defaults).

+

The default setup @local_domains_acl = ( ".$mydomain" ); does not match subdomains. To match subdomains, you can override the @local_domains_acl directive in the amavis user config file 50-user with @local_domains_maps = ("."); to match any sort of domain template.

+

How can I make SpamAssassin better recognize spam?

+

Put received spams in .Junk/ imap folder using SPAMASSASSIN_SPAM_TO_INBOX=1 and MOVE_SPAM_TO_JUNK=1 and add a user cron like the following:

+
# This assumes you're having `environment: ONE_DIR=1` in the `mailserver.env`,
+# with a consolidated config in `/var/mail-state`
+#
+# m h dom mon dow command
+# Everyday 2:00AM, learn spam from a specific user
+0 2 * * * docker exec mail sa-learn --spam /var/mail/domain.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin
+
+

If you run the server with docker-compose, you can leverage on docker configs and the mailserver's own cron. This is less problematic than the simple solution shown above, because it decouples the learning from the host on which the mailserver is running and avoids errors if the server is not running.

+

The following configuration works nicely:

+
Example

Create a system cron file:

+
# in the docker-compose.yml root directory
+mkdir cron
+touch cron/sa-learn
+chown root:root cron/sa-learn
+chmod 0644 cron/sa-learn
+
+

Edit the system cron file nano cron/sa-learn, and set an appropriate configuration:

+
# This assumes you're having `environment: ONE_DIR=1` in the env-mailserver,
+# with a consolidated config in `/var/mail-state`
+#
+# m h dom mon dow user command
+#
+# Everyday 2:00AM, learn spam from a specific user
+# spam: junk directory
+0  2 * * * root  sa-learn --spam /var/mail/domain.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin
+# ham: archive directories
+15 2 * * * root  sa-learn --ham /var/mail/domain.com/username/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin
+# ham: inbox subdirectories
+30 2 * * * root  sa-learn --ham /var/mail/domain.com/username/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin
+#
+# Everyday 3:00AM, learn spam from all users of a domain
+# spam: junk directory
+0  3 * * * root  sa-learn --spam /var/mail/otherdomain.com/*/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin
+# ham: archive directories
+15 3 * * * root  sa-learn --ham /var/mail/otherdomain.com/*/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin
+# ham: inbox subdirectories
+30 3 * * * root  sa-learn --ham /var/mail/otherdomain.com/*/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin
+
+

Then with plain docker-compose:

+
services:
+  mail:
+    image: mailserver/docker-mailserver:latest
+    volumes:
+      - ./cron/sa-learn:/etc/cron.d/sa-learn
+
+

Or with docker swarm:

+
version: "3.3"
+
+services:
+  mail:
+    image: mailserver/docker-mailserver:latest
+    # ...
+    configs:
+      - source: my_sa_crontab
+        target: /etc/cron.d/sa-learn
+
+configs:
+  my_sa_crontab:
+    file: ./cron/sa-learn
+
+
+

With the default settings, SpamAssassin will require 200 mails trained for spam (for example with the method explained above) and 200 mails trained for ham (using the same command as above but using --ham and providing it with some ham mails). Until you provided these 200+200 mails, SpamAssassin will not take the learned mails into account. For further reference, see the SpamAssassin Wiki.

+

How can I configure a catch-all?

+

Considering you want to redirect all incoming e-mails for the domain domain.tld to user1@domain.tld, add the following line to config/postfix-virtual.cf:

+
@domain.tld user1@domain.tld
+
+

How can I delete all the emails for a specific user?

+

First of all, create a special alias named devnull by editing config/postfix-aliases.cf:

+
devnull: /dev/null
+
+

Considering you want to delete all the e-mails received for baduser@domain.tld, add the following line to config/postfix-virtual.cf:

+
baduser@domain.tld devnull
+
+

How do I have more control about what SPAMASSASIN is filtering?

+

By default, SPAM and INFECTED emails are put to a quarantine which is not very straight forward to access. Several config settings are affecting this behavior:

+

First, make sure you have the proper thresholds set:

+
SA_TAG=-100000.0
+SA_TAG2=3.75
+SA_KILL=100000.0
+
+
    +
  • The very negative vaue in SA_TAG makes sure, that all emails have the SpamAssassin headers included.
  • +
  • SA_TAG2 is the actual threshold to set the YES/NO flag for spam detection.
  • +
  • SA_KILL needs to be very high, to make sure nothing is bounced at all (SA_KILL superseeds SPAMASSASSIN_SPAM_TO_INBOX)
  • +
+

Make sure everything (including SPAM) is delivered to the inbox and not quarantined:

+
SPAMASSASSIN_SPAM_TO_INBOX=1
+
+

Use MOVE_SPAM_TO_JUNK=1 or create a sieve script which puts spam to the Junk folder:

+
require ["comparator-i;ascii-numeric","relational","fileinto"];
+
+if header :contains "X-Spam-Flag" "YES" {
+  fileinto "Junk";
+} elsif allof (
+   not header :matches "x-spam-score" "-*",
+   header :value "ge" :comparator "i;ascii-numeric" "x-spam-score" "3.75" ) {
+  fileinto "Junk";
+}
+
+

Create a dedicated mailbox for emails which are infected/bad header and everything amavis is blocking by default and put its address into config/amavis.cf

+
$clean_quarantine_to      = "amavis\@domain.com";
+$virus_quarantine_to      = "amavis\@domain.com";
+$banned_quarantine_to     = "amavis\@domain.com";
+$bad_header_quarantine_to = "amavis\@domain.com";
+$spam_quarantine_to       = "amavis\@domain.com";
+
+

What kind of SSL certificates can I use?

+

You can use the same certificates you use with another mail server.

+

The only thing is that we provide a self-signed certificate tool and a letsencrypt certificate loader.

+

I just moved from my old mail server, but "it doesn't work"?

+

If this migration implies a DNS modification, be sure to wait for DNS propagation before opening an issue. +Few examples of symptoms can be found here or here.

+

This could be related to a modification of your MX record, or the IP mapped to mail.my-domain.tld. Additionally, validate your DNS configuration.

+

If everything is OK regarding DNS, please provide formatted logs and config files. This will allow us to help you.

+

If we're blind, we won't be able to do anything.

+

What system requirements are required to run docker-mailserver effectively?

+

1 core and 1GB of RAM + swap partition is recommended to run docker-mailserver with clamav. +Otherwise, it could work with 512M of RAM.

+
+

Warning

+

Clamav can consume a lot of memory, as it reads the entire signature database into RAM.

+

Current figure is about 850M and growing. If you get errors about clamav or amavis failing to allocate memory you need more RAM or more swap and of course docker must be allowed to use swap (not always the case). If you can't use swap at all you may need 3G RAM.

+
+

Can docker-mailserver run in a Rancher Environment?

+

Yes, by adding the environment variable PERMIT_DOCKER: network.

+
+

Warning

+

Adding the docker network's gateway to the list of trusted hosts, e.g. using the network or connected-networks option, can create an open relay, for instance if IPv6 is enabled on the host machine but not in Docker.

+
+

How can I Authenticate Users with SMTP_ONLY?

+

See #1247 for an example.

+
+

Todo

+

Write a How-to / Use-Case / Tutorial about authentication with SMTP_ONLY.

+
+

Common Errors

+
warning: connect to Milter service inet:localhost:8893: Connection refused
+# DMARC not running
+# => /etc/init.d/opendmarc restart
+
+warning: connect to Milter service inet:localhost:8891: Connection refused
+# DKIM not running
+# => /etc/init.d/opendkim restart
+
+mail amavis[1459]: (01459-01) (!)connect to /var/run/clamav/clamd.ctl failed, attempt #1: Can't connect to a UNIX socket /var/run/clamav/clamd.ctl: No such file or directory
+mail amavis[1459]: (01459-01) (!)ClamAV-clamd: All attempts (1) failed connecting to /var/run/clamav/clamd.ctl, retrying (2)
+mail amavis[1459]: (01459-01) (!)ClamAV-clamscan av-scanner FAILED: /usr/bin/clamscan KILLED, signal 9 (0009) at (eval 100) line 905.
+mail amavis[1459]: (01459-01) (!!)AV: ALL VIRUS SCANNERS FAILED
+# Clamav is not running (not started or because you don't have enough memory)
+# => check requirements and/or start Clamav
+
+

How to use when behind a Proxy

+

Add to /etc/postfix/main.cf :

+
proxy_interfaces = X.X.X.X (your public IP)
+
+

What About Updates

+

You can of course use a own script or every now and then pull && stop && rm && start the images but there are tools available for this. +There is a section in the Update and Cleanup documentation page that explains how to use it the docker way.

+

How to adjust settings with the user-patches.sh script

+

Suppose you want to change a number of settings that are not listed as variables or add things to the server that are not included?

+

This docker-container has a built-in way to do post-install processes. If you place a script called user-patches.sh in the config directory it will be run after all configuration files are set up, but before the postfix, amavis and other daemons are started.

+

The config file I am talking about is this volume in the yml file: ./config/:/tmp/docker-mailserver/

+

To place such a script you can just make it in the config dir, for instance like this:

+
cd ./config
+touch user-patches.sh
+chmod +x user-patches.sh
+
+

Then fill user-patches.sh with suitable code.

+

If you want to test it you can move into the running container, run it and see if it does what you want. For instance:

+
# start shell in container
+./setup.sh debug login
+
+# check the file
+cat /tmp/docker-mailserver/user-patches.sh
+
+# run the script
+/tmp/docker-mailserver/user-patches.sh
+
+# exit the container shell back to the host shell
+exit
+
+

You can do a lot of things with such a script. You can find an example user-patches.sh script here: example user-patches.sh script

+

Special use-case - Patching the supervisord config

+

It seems worth noting, that the user-patches.sh gets executed trough supervisord. If you need to patch some supervisord config (e.g. /etc/supervisor/conf.d/saslauth.conf), the patching happens too late.

+

An easy workaround is to make the user-patches.sh reload the supervisord config after patching it:

+
#!/bin/bash
+sed -i 's/rimap -r/rimap/' /etc/supervisor/conf.d/saslauth.conf
+supervisorctl update
+
+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/favicon.ico b/v10.0/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7badbc707fdc8169abe67f7f2d9fb4f2f5cc0df9 GIT binary patch literal 15086 zcmeHOU2GIp6u!-x)VJc3F-F@5O-xMeEQCM1-R*9HRvLap3mS; zc_1490-{JX0c}bIe+WFVi2^Z%M4B`)J~S9fP)y8Lwxz(*>-p}?p?7a*X6I+ONZd^h zXXehm=X~co=bl;bRv{WhLRgj{-6HniFT^|{L`#cvKTC)a8k0h&Wg z#0COj$CFV2u-Ns#f^C?1De|*70tAYOdV%+QJiah0~7FG_&BYkurDhiV_)Qy@I5ro8Tl}y)*z;7u~3b9@fLR+wac7? zN5;)q!)lHPcEAJMMn2#xE?#{-cxU_Y1>a%udd=tDAP(@teo&)2hB()}M`6F3*!7q= z4!y3NeI9F%qwj$e)M~A;n{l}0B61AOJ$NzwaK53zpZ+}VHMlC<7gWPxYl!zOxHs`3 z9iAiHGq&zIx^O`QLx534SvM)Iyl|1lJDg(Zr8?#D(0r#pgCZ z>KO9CwMv|x2~tOhlk3B*ov1kCtiyYc-*@OKm15wYk2P`pR&y=tfH|#HN4$5Dw`yXD zV+(a)^d$AY)-4ttKXP|7MXz6dFK{lP&6;BT`kb42t@jATfLcNi-SOc^P0?_(IV_Ke|+xP_aKfVXswubdJc`RWk0O_Fzfh{3tr==;luRHdjR;O)(vce8~6^2S;voS zKitl4g19w#3*#_6Mi7V)Rr5s>yv9DTadF%YokWy?oVr92kPc;+6xw=hqr%CHugFUgcL`dJf!rekC6> z_1tgI^FDU#`7?ztj30B)J2~*|`C)9&N{%YGG~lm#My z=E;(y<)`*N(W1uvHU3Vkg`|Vz#@}ht-*VMIbp~z~26FAQ@1p$VY{%eBhc?An1}hm>+mv^i;x?_KLp<+bS@m_ zHx|YLK2&a~W`gF#$+zO0eS>=l-v`yefxLhV)J6~th#k1u2lzw`uz_nH52JxS2nHwb zWw9a$@c-GK4Oj3R9txUi0*;n8a`Lhb(($`3nGgTTsnbzS6IkD_~$xwdL%e@ ztp}{bdfhKy2l0)$%Z^NZZu&j1ud3y3Xw2noe zhimW$+uU1j^kg5ujHTzB@f*ME=|0z;E&nY8&rH9{_h9i)wgqj+Oz&!~=Jn_1bvLc& zI8C4WJ$P{Q&)l5fAP(5~@{Mtm1Frk1 ze%;m~9zRZW4p2L8HmWrS`)-_Y4!~QreEHeo{BWHBlUa9u>#O+;`GN0h)Lk`k)olf1 z0H+AkK0E2uflgHlbmVf5jwSMRgpn6GvJicaWXO6m37vq5h7ui^ltiLoLL{0dL}N2a z2FDr&I)R~QQua5~aRy0=L>?!Z`rw=b#}X|tr2eG~`hU&-%b&#?-@HB7Zavi5W-W%s zeGt5uwqFJm5X=)BELT^DptgdF^+}4n{;&z6zRJ|QKt9M_c=or9k}!> z_yb4aLHQ%@czw^|@sF^UdajbSKjSxj+a5HYBkmQ8X8#4uypDD6iPA-^(FL~H#iR?9AX@{;ls3p^}vofK|k=-`@^-tCr)1<1BNAE6<-Rv)||KMN4Y@HxA{Eoy?y3M oO^D7u^L=zKOWmiR&S_N#a?WYb-9gXj4V=%=c`!+d=JC3J0iF@M>i_@% literal 0 HcmV?d00001 diff --git a/v10.0/index.html b/v10.0/index.html new file mode 100644 index 00000000..be7a320f --- /dev/null +++ b/v10.0/index.html @@ -0,0 +1,1387 @@ + + + + + + + + + + + + + + + + + + + + + + + Home - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Welcome to the Extended Documentation for docker-mailserver!

+

Please first have a look at the README.md to setup and configure this server.

+

This documentation provides you with advanced configuration, detailed examples, and hints.

+

Getting Started

+
    +
  1. The script setup.sh is supplied with this project. It supports you in configuring and administrating your server. Information on how to get it and how to use it is available on a dedicated page.
  2. +
  3. Be aware that advanced tasks may still require tweaking environment variables, reading through documentation and sometimes inspecting your running container for debugging purposes. After all, a mail server is a complex arrangement of various programs.
  4. +
  5. A list of all configuration options is documented on the ENV page. The README.md is a good starting point to understand what this image is capable of.
  6. +
  7. A list of all optional and automatically created configuration files and directories is available on the dedicated page.
  8. +
+
+

Tip

+

See the FAQ for some more tips!

+
+
+

Important

+

If you'd like to change, patch or alter files or behavior of docker-mailserver, you can use a script. Just place it in the config/ folder that is created on startup and call it user-patches.sh. If you'd like to see the full documentation and an example, visit the 'Modifications via Script' page.

+
+

Contributing

+

We are always happy to welcome new contributors. For guidelines and entrypoints please have a look at the Contributing section.

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/introduction/index.html b/v10.0/introduction/index.html new file mode 100644 index 00000000..4b654403 --- /dev/null +++ b/v10.0/introduction/index.html @@ -0,0 +1,1717 @@ + + + + + + + + + + + + + + + + + + + + + + + An Introduction to Mail Servers - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+ +
+ + +
+
+ + + + + + + +

An Introduction to Mail Servers

+

What is a mail server and how does it perform its duty?

+

Here's an introduction to the field that covers everything you need to know to get started with docker-mailserver.

+

Anatomy of a Mail Server

+

A mail server is only a part of a client-server relationship aimed at exchanging information in the form of emails. Exchanging emails requires using specific means (programs and protocols).

+

docker-mailserver provides you with the server portion, whereas the client can be anything from a terminal via text-based software (eg. Mutt) to a fully-fledged desktop application (eg. Mozilla Thunderbird, Microsoft Outlook…), to a web interface, etc.

+

Unlike the client-side where usually a single program is used to perform retrieval and viewing of emails, the server-side is composed of many specialized components. The mail server is capable of accepting, forwarding, delivering, storing and overall exchanging messages, but each one of those tasks is actually handled by a specific piece of software. All of these "agents" must be integrated with one another for the exchange to take place.

+

docker-mailserver has made informed choices about those components and their (default) configuration. It offers a comprehensive platform to run a fully featured mail server in no time!

+

Components

+

The following components are required to create a complete delivery chain:

+
    +
  • MUA: a Mail User Agent is basically any client/program capable of sending emails to arbitrary mail servers; while also capable of fetching emails from mail servers for presenting them to the end users.
  • +
  • MTA: a Mail Transfer Agent is the so-called "mail server" as seen from the MUA's perspective. It's a piece of software dedicated to accepting submitted emails, then forwarding them-where exactly will depend on an email's final destination. If the receiving MTA is responsible for the hostname the email is sent to, then an MTA is to forward that email to an MDA (see below). Otherwise, it is to transfer (ie. forward, relay) to another MTA, "closer" to the email's final destination.
  • +
  • MDA: a Mail Delivery Agent is responsible for accepting emails from an MTA and dropping them into their recipients' mailboxes, whichever the form.
  • +
+

Here's a schematic view of mail delivery:

+
Sending an email:    MUA ----> MTA ----> (MTA relays) ----> MDA
+Fetching an email:   MUA <--------------------------------- MDA
+
+

There may be other moving parts or sub-divisions (for instance, at several points along the chain, specialized programs may be analyzing, filtering, bouncing, editing… the exchanged emails).

+

In a nutshell, docker-mailserver provides you with the following components:

+
    +
  • A MTA: Postfix
  • +
  • A MDA: Dovecot
  • +
  • A bunch of additional programs to improve security and emails processing
  • +
+

Here's where docker-mailserver's toochain fits within the delivery chain:

+
                                    docker-mailserver is here:
+                                                         ┏━━━━━━━┓
+Sending an email:    MUA ---> MTA ---> (MTA relays) ---> ┫ MTA ╮ ┃
+Fetching an email:   MUA <------------------------------ ┫ MDA ╯ ┃
+                                                         ┗━━━━━━━┛
+
+
+

Example

+

Let's say Alice owns a Gmail account, alice@gmail.com; and Bob owns an account on a docker-mailserver's instance, bob@dms.io.

+

Make sure not to conflate these two very different scenarios: +A) Alice sends an email to bob@dms.io => the email is first submitted to MTA smtp.gmail.com, then relayed to MTA smtp.dms.io where it is then delivered into Bob's mailbox. +B) Bob sends an email to alice@gmail.com => the email is first submitted to MTA smtp.dms.io, then relayed to MTA smtp.gmail.com and eventually delivered into Alice's mailbox.

+

In scenario A the email leaves Gmail's premises, that email's initial submission is not handled by your docker-mailserver instance(MTA); it merely receives the email after it has been relayed by Gmail's MTA. In scenario B, the docker-mailserver instance(MTA) handles the submission, prior to relaying.

+

The main takeaway is that when a third-party sends an email to a docker-mailserver instance(MTA) (or any MTA for that matter), it does not establish a direct connection with that MTA. Email submission first goes through the sender's MTA, then some relaying between at least two MTAs is required to deliver the email. That will prove very important when it comes to security management.

+
+

One important thing to note is that MTA and MDA programs may actually handle multiple tasks (which is the case with docker-mailserver's Postfix and Dovecot).

+

For instance, Postfix is both an SMTP server (accepting emails) and a relaying MTA (transferring, ie. sending emails to other MTA/MDA); Dovecot is both an MDA (delivering emails in mailboxes) and an IMAP server (allowing MUAs to fetch emails from the mail server). On top of that, Postfix may rely on Dovecot's authentication capabilities.

+

The exact relationship between all the components and their respective (sometimes shared) responsibilities is beyond the scope of this document. Please explore this wiki & the web to get more insights about docker-mailserver's toolchain.

+

About Security & Ports

+

In the previous section, different components were outlined. Each one of those is responsible for a specific task, it has a specific purpose.

+

Three main purposes exist when it comes to exchanging emails:

+
    +
  • Submission: for a MUA (client), the act of sending actual email data over the network, toward an MTA (server).
  • +
  • Transfer (aka. Relay): for an MTA, the act of sending actual email data over the network, toward another MTA (server) closer to the final destination (where an MTA will forward data to an MDA).
  • +
  • Retrieval: for a MUA (client), the act of fetching actual email data over the network, from an MDA.
  • +
+

Postfix handles Submission (and might handle Relay), whereas Dovecot handles Retrieval. They both need to be accessible by MUAs in order to act as servers, therefore they expose public endpoints on specific TCP ports (see. Understanding the ports for more details). Those endpoints may be secured, using an encryption scheme and TLS certificates.

+

When it comes to the specifics of email exchange, we have to look at protocols and ports enabled to support all the identified purposes. There are several valid options and they've been evolving overtime.

+

Here's docker-mailserver's default configuration:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PurposeProtocolTCP port / encryption
Transfer/RelaySMTP25 (unencrypted)
SubmissionESMTP587 (encrypted using STARTTLS)
RetrievalIMAP4143 (encrypted using STARTTLS) + 993 (TLS)
RetrievalPOP3Not activated
+
 ┏━━━━━━━━━━ Submission ━━━━━━━━━┓┏━━━━━━━━━━━━━ Transfer/Relay ━━━━━━━━━━━┓
+                        ┌─────────────────────┐                    ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
+MUA ----- STARTTLS ---> ┤(587)   MTA ╮    (25)├ <-- cleartext ---> ┊ Third-party MTA ┊
+    ---- cleartext ---> ┤(25)        │        |                    └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
+                        |┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄|
+MUA <---- STARTTLS ---- ┤(143)   MDA ╯        |
+    <-- enforced TLS -- ┤(993)                |
+                        └─────────────────────┘
+ ┗━━━━━━━━━━ Retrieval ━━━━━━━━━━┛
+
+

If you're new to email infrastructure, both that table and the schema may be confusing.
+Read on to expand your understanding and learn about docker-mailserver's configuration, including how you can customize it.

+

Submission - SMTP

+

For a MUA to send an email to an MTA, it needs to establish a connection with that server, then push data packets over a network that both the MUA (client) and the MTA (server) are connected to. The server implements the SMTP protocol, which makes it capable of handling Submission.

+

In the case of docker-mailserver, the MTA (SMTP server) is Postfix. The MUA (client) may vary, yet its Submission request is performed as TCP packets sent over the public internet. This exchange of information may be secured in order to counter eavesdropping.

+

Two kinds of Submission

+

Let's say I own an account on a docker-mailserver instance, me@dms.io. There are two very different use-cases for Submission:

+
    +
  1. I want to send an email to someone
  2. +
  3. Someone wants to send you an email
  4. +
+

In the first scenario, I will be submitting my email directly to my docker-mailserver instance/MTA (Postfix), which will then relay the email to its recipient's MTA for final delivery. In this case, Submission is first handled by establishing a direct connection to my own MTA-so at least for this portion of the delivery chain, I'll be able to ensure security/confidentiality. Not so much for what comes next, ie. relaying between MTAs and final delivery.

+

In the second scenario, a third-party email account owner will be first submitting an email to some third-party MTA. I have no control over this initial portion of the delivery chain, nor do I have control over the relaying that comes next. My MTA will merely accept a relayed email coming "out of the blue".

+

My MTA will thus have to support two kinds of Submission:

+
    +
  • Outward Submission (self-owned email is submitted directly to the MTA, then is relayed "outside")
  • +
  • Inward Submission (third-party email has been submitted & relayed, then is accepted "inside" by the MTA)
  • +
+
 ┏━━━━ Outward Submission ━━━━┓
+                    ┌────────────────────┐                    ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
+Me ---------------> ┤                    ├ -----------------> ┊                 ┊
+                    │       My MTA       │                    ┊ Third-party MTA ┊
+                    │                    ├ <----------------- ┊                 ┊
+                    └────────────────────┘                    └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
+                               ┗━━━━━━━━━━ Inward Submission ━━━━━━━━━━┛
+
+
Outward Submission
+

The best practice as of 2020 when it comes to securing Outward Submission is to use Implicit TLS connection via ESMTP on port 465 (see RFC 8314). Let's break it down.

+
    +
  • Implicit TLS means the server enforces the client into using an encrypted TCP connection, using TLS. With this kind of connection, the MUA has to establish a TLS-encrypted connection from the get go (TLS is implied, hence the name "Implicit"). Any client attempting to either submit email in cleartext (unencrypted, not secure), or requesting a cleartext connection to be upgraded to a TLS-encrypted one using STARTTLS, is to be denied. Implicit TLS is sometimes called Enforced TLS for that reason.
  • +
  • ESMTP is SMTP + extensions. It's the version of the SMTP protocol that most mail servers speak nowadays. For the purpose of this documentation, ESMTP and SMTP are synonymous.
  • +
  • Port 465 is the reserved TCP port for Implicit TLS Submission (since 2018). There is actually a boisterous history to that ports usage, but let's keep it simple.
  • +
+
+

Warning

+

This Submission setup is sometimes refered to as SMTPS. Long story short: this is incorrect and should be avoided.

+
+

Although a very satisfactory setup, Implicit TLS on port 465 is somewhat "cutting edge". There exists another well established mail Submission setup that must be supported as well, SMTP+STARTTLS on port 587. It uses Explicit TLS: the client starts with a cleartext connection, then the server informs a TLS-encrypted "upgraded" connection may be established, and the client may eventually decide to establish it prior to the Submission. Basically it's an opportunistic, opt-in TLS upgrade of the connection between the client and the server, at the client's discretion, using a mechanism known as STARTTLS that both ends need to implement.

+

In many implementations, the mail server doesn't enforce TLS encryption, for backwards compatibility. Clients are thus free to deny the TLS-upgrade proposal (or misled by a hacker about STARTTLS not being available), and the server accepts unencrypted (cleartext) mail exchange, which poses a confidentiality threat and, to some extent, spam issues. RFC 8314 (section 3.3) recommends for mail servers to support both Implicit and Explicit TLS for Submission, and to enforce TLS-encryption on ports 587 (Explicit TLS) and 465 (Implicit TLS). That's exactly docker-mailserver's default configuration: abiding by RFC 8314, it enforces a strict (encrypt) STARTTLS policy, where a denied TLS upgrade terminates the connection thus (hopefully but at the client's discretion) preventing unencrypted (cleartext) Submission.

+
    +
  • docker-mailserver's default configuration enables and requires Explicit TLS (STARTTLS) on port 587 for Outward Submission.
  • +
  • It does not enable Implicit TLS Outward Submission on port 465 by default. One may enable it through simple custom configuration, either as a replacement or (better!) supplementary mean of secure Submission.
  • +
  • It does not support old MUAs (clients) not supporting TLS encryption on ports 587/465 (those should perform Submission on port 25, more details below). One may relax that constraint through advanced custom configuration, for backwards compatibility.
  • +
+

A final Outward Submission setup exists and is akin SMTP+STARTTLS on port 587, but on port 25. That port has historically been reserved specifically for unencrypted (cleartext) mail exchange though, making STARTTLS a bit wrong to use. As is expected by RFC 5321, docker-mailserver uses port 25 for unencrypted Submission in order to support older clients, but most importantly for unencrypted Transfer/Relay between MTAs.

+
    +
  • docker-mailserver's default configuration also enables unencrypted (cleartext) on port 25 for Outward Submission.
  • +
  • It does not enable Explicit TLS (STARTTLS) on port 25 by default. One may enable it through advanced custom configuration, either as a replacement (bad!) or as a supplementary mean of secure Outward Submission.
  • +
  • One may also secure Outward Submission using advanced encryption scheme, such as DANE/DNSSEC and/or MTA-STS.
  • +
+
Inward Submission
+

Granted it's still very difficult enforcing encryption between MTAs (Transfer/Relay) without risking dropping emails (when relayed by MTAs not supporting TLS-encryption), Inward Submission is to be handled in cleartext on port 25 by default.

+
    +
  • docker-mailserver's default configuration enables unencrypted (cleartext) on port 25 for Inward Submission.
  • +
  • It does not enable Explicit TLS (STARTTLS) on port 25 by default. One may enable it through advanced custom configuration, either as a replacement (bad!) or as a supplementary mean of secure Inward Submission.
  • +
  • One may also secure Inward Submission using advanced encryption scheme, such as DANE/DNSSEC and/or MTA-STS.
  • +
+

Overall, docker-mailserver's default configuration for SMTP looks like this:

+
 ┏━━━━ Outward Submission ━━━━┓
+                    ┌────────────────────┐                    ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
+Me -- cleartext --> ┤(25)            (25)├ --- cleartext ---> ┊                 ┊
+Me -- STARTTLS ---> ┤(587)  My MTA       │                    ┊ Third-party MTA ┊
+                    │                (25)├ <---cleartext ---- ┊                 ┊
+                    └────────────────────┘                    └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
+                               ┗━━━━━━━━━━ Inward Submission ━━━━━━━━━━┛
+
+

Retrieval - IMAP

+

A MUA willing to fetch an email from a mail server will most likely communicate with its IMAP server. As with SMTP described earlier, communication will take place in the form of data packets exchanged over a network that both the client and the server are connected to. The IMAP protocol makes the server capable of handling Retrieval.

+

In the case of docker-mailserver, the IMAP server is Dovecot. The MUA (client) may vary, yet its Retrieval request is performed as TCP packets sent over the public internet. This exchange of information may be secured in order to counter eavesdropping.

+

Again, as with SMTP described earlier, the IMAP protocol may be secured with either Implicit TLS (aka. IMAPS / IMAP4S) or Explicit TLS (using STARTTLS).

+

The best practice as of 2020 is to enforce IMAPS on port 993, rather than IMAP+STARTTLS on port 143 (see RFC 8314); yet the latter is usually provided for backwards compatibility.

+

docker-mailserver's default configuration enables both Implicit and Explicit TLS for Retrievial, on ports 993 and 143 respectively.

+

Retrieval - POP3

+

Similarly to IMAP, the older POP3 protocol may be secured with either Implicit or Explicit TLS.

+

The best practice as of 2020 would be POP3S on port 995, rather than POP3+STARTTLS on port 110 (see RFC 8314).

+

docker-mailserver's default configuration disables POP3 altogether. One should expect MUAs to use TLS-encrypted IMAP for Retrieval.

+

How does docker-mailserver help with setting everything up?

+

As a batteries included Docker image, docker-mailserver provides you with all the required components and a default configuration, to run a decent and secure mail server.

+

One may then customize all aspects of its internal components.

+ +

On the subject of security, one might consider docker-mailserver's default configuration to not be 100% secure:

+
    +
  • it enables unencrypted traffic on port 25
  • +
  • it enables Explicit TLS (STARTTLS) on port 587, instead of Implicit TLS on port 465
  • +
+

We believe docker-mailserver's default configuration to be a good middle ground: it goes slightly beyond "old" (1999) RFC 2487; and with developer friendly configuration settings, it makes it pretty easy to abide by the "newest" (2018) RFC 8314.

+

Eventually, it is up to you deciding exactly what kind of transportation/encryption to use and/or enforce, and to customize your instance accordingly (with looser or stricter security). Be also aware that protocols and ports on your server can only go so far with security; third-party MTAs might relay your emails on insecure connections, man-in-the-middle attacks might still prove effective, etc. Advanced counter-measure such as DANE, MTA-STS and/or full body encryption (eg. PGP) should be considered as well for increased confidentiality, but ideally without compromising backwards compatibility so as to not block emails.

+

The README is the best starting point in configuring and running your mail server. You may then explore this wiki to cover additional topics, including but not limited to, security.

+ + + + + + + + + +
+
+
+ + + + + +
+ + + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v10.0/search/search_index.json b/v10.0/search/search_index.json new file mode 100644 index 00000000..3ab55238 --- /dev/null +++ b/v10.0/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"Welcome to the Extended Documentation for docker-mailserver ! Please first have a look at the README.md to setup and configure this server. This documentation provides you with advanced configuration, detailed examples, and hints. Getting Started The script setup.sh is supplied with this project. It supports you in configuring and administrating your server. Information on how to get it and how to use it is available on a dedicated page . Be aware that advanced tasks may still require tweaking environment variables, reading through documentation and sometimes inspecting your running container for debugging purposes. After all, a mail server is a complex arrangement of various programs. A list of all configuration options is documented on the ENV page . The README.md is a good starting point to understand what this image is capable of. A list of all optional and automatically created configuration files and directories is available on the dedicated page . Tip See the FAQ for some more tips! Important If you'd like to change, patch or alter files or behavior of docker-mailserver , you can use a script. Just place it in the config/ folder that is created on startup and call it user-patches.sh . If you'd like to see the full documentation and an example, visit the 'Modifications via Script' page . Contributing We are always happy to welcome new contributors. For guidelines and entrypoints please have a look at the Contributing section .","title":"Home"},{"location":"#welcome-to-the-extended-documentation-for-docker-mailserver","text":"Please first have a look at the README.md to setup and configure this server. This documentation provides you with advanced configuration, detailed examples, and hints.","title":"Welcome to the Extended Documentation for docker-mailserver!"},{"location":"#getting-started","text":"The script setup.sh is supplied with this project. It supports you in configuring and administrating your server. Information on how to get it and how to use it is available on a dedicated page . Be aware that advanced tasks may still require tweaking environment variables, reading through documentation and sometimes inspecting your running container for debugging purposes. After all, a mail server is a complex arrangement of various programs. A list of all configuration options is documented on the ENV page . The README.md is a good starting point to understand what this image is capable of. A list of all optional and automatically created configuration files and directories is available on the dedicated page . Tip See the FAQ for some more tips! Important If you'd like to change, patch or alter files or behavior of docker-mailserver , you can use a script. Just place it in the config/ folder that is created on startup and call it user-patches.sh . If you'd like to see the full documentation and an example, visit the 'Modifications via Script' page .","title":"Getting Started"},{"location":"#contributing","text":"We are always happy to welcome new contributors. For guidelines and entrypoints please have a look at the Contributing section .","title":"Contributing"},{"location":"faq/","text":"What kind of database are you using? None! No database is required. Filesystem is the database. This image is based on config files that can be persisted using Docker volumes, and as such versioned, backed up and so forth. Where are emails stored? Mails are stored in /var/mail/${domain}/${username} . Since v9.0.0 it is possible to add custom user_attributes for each accounts to have a different mailbox configuration (See #1792 ). Warning You should use a data volume container for /var/mail to persist data. Otherwise, your data may be lost. How to alter the running mailserver instance without relaunching the container? docker-mailserver aggregates multiple \"sub-services\", such as Postfix, Dovecot, Fail2ban, SpamAssassin, etc. In many cases, one may edit a sub-service's config and reload that very sub-service, without stopping and relaunching the whole mail server. In order to do so, you'll probably want to push your config updates to your server through a Docker volume, then restart the sub-service to apply your changes, using supervisorctl . For instance, after editing fail2ban's config: supervisorctl restart fail2ban . See supervisorctl's documentation . Tip To add, update or delete an email account; there is no need to restart postfix / dovecot service inside the container after using setup.sh script. For more information, see #1639 . How can I sync container with host date/time? Timezone? Share the host's /etc/localtime with the docker-mailserver container, using a Docker volume: volumes : - /etc/localtime:/etc/localtime:ro Optional Add one line to .env or env-mailserver to set timetzone for container, for example: TZ = Europe/Berlin Check here for the tz name list What is the file format? All files are using the Unix format with LF line endings. Please do not use CRLF . What about backups? Assuming that you use docker-compose and data volumes, you can backup the configuration, emails and logs like this: # create backup docker run --rm -ti \\ -v maildata:/var/mail \\ -v mailstate:/var/mail-state \\ -v maillogs:/var/logs/mail \\ -v \" $PWD /config\" :/tmp/docker-mailserver \\ -v /backup/mail:/backup \\ alpine:latest \\ tar czf \"/backup/mail- $( date +%F ) .tar.gz\" /var/mail /var/mail-state /var/logs/mail /tmp/docker-mailserver # delete backups older than 30 days find /backup/mail -type f -mtime +30 -delete What about mail-state folder? This folder consolidates all data generated by the server itself to persist when you upgrade. Example of data folder persisted: lib-amavis, lib-clamav, lib-fail2ban, lib-postfix, lib-postgrey, lib-spamassasin, lib-spamassassin, spool-postfix, ... How can I configure my email client? Login are full email address ( user@domain.com ). # imap username : password : server : imap port : 143 or 993 with ssl (recommended) imap path prefix : INBOX # smtp smtp port : 25 or 587 with ssl (recommended) username : password : Please use STARTTLS . How can I manage my custom SpamAssassin rules? Antispam rules are managed in config/spamassassin-rules.cf . What are acceptable SA_SPAM_SUBJECT values? For no subject set SA_SPAM_SUBJECT=undef . For a trailing white-space subject one can define the whole variable with quotes in docker-compose.yml : environment : - \"SA_SPAM_SUBJECT=[SPAM] \" Can I use naked/bare domains (no host name)? Yes, but not without some configuration changes. Normally it is assumed that docker-mailserver runs on a host with a name, so the fully qualified host name might be mail.example.com with the domain example.com . The MX records point to mail.example.com . To use a bare domain where the host name is example.com and the domain is also example.com , change mydestination : From: mydestination = $myhostname, localhost.$mydomain, localhost To: mydestination = localhost.$mydomain, localhost Add the latter line to config/postfix-main.cf . That should work. Without that change there will be warnings in the logs like: warning: do not list domain example.com in BOTH mydestination and virtual_mailbox_domains Plus of course mail delivery fails. Why are SpamAssassin x-headers not inserted into my sample.domain.com subdomain emails? In the default setup, amavis only applies SpamAssassin x-headers into domains matching the template listed in the config file ( 05-domain_id in the amavis defaults). The default setup @local_domains_acl = ( \".$mydomain\" ); does not match subdomains. To match subdomains, you can override the @local_domains_acl directive in the amavis user config file 50-user with @local_domains_maps = (\".\"); to match any sort of domain template. How can I make SpamAssassin better recognize spam? Put received spams in .Junk/ imap folder using SPAMASSASSIN_SPAM_TO_INBOX=1 and MOVE_SPAM_TO_JUNK=1 and add a user cron like the following: # This assumes you're having `environment: ONE_DIR=1` in the `mailserver.env`, # with a consolidated config in `/var/mail-state` # # m h dom mon dow command # Everyday 2:00AM, learn spam from a specific user 0 2 * * * docker exec mail sa-learn --spam /var/mail/domain.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin If you run the server with docker-compose , you can leverage on docker configs and the mailserver's own cron. This is less problematic than the simple solution shown above, because it decouples the learning from the host on which the mailserver is running and avoids errors if the server is not running. The following configuration works nicely: Example Create a system cron file: # in the docker-compose.yml root directory mkdir cron touch cron/sa-learn chown root:root cron/sa-learn chmod 0644 cron/sa-learn Edit the system cron file nano cron/sa-learn , and set an appropriate configuration: # This assumes you're having `environment: ONE_DIR=1` in the env-mailserver, # with a consolidated config in `/var/mail-state` # # m h dom mon dow user command # # Everyday 2:00AM, learn spam from a specific user # spam: junk directory 0 2 * * * root sa-learn --spam /var/mail/domain.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin # ham: archive directories 15 2 * * * root sa-learn --ham /var/mail/domain.com/username/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin # ham: inbox subdirectories 30 2 * * * root sa-learn --ham /var/mail/domain.com/username/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin # # Everyday 3:00AM, learn spam from all users of a domain # spam: junk directory 0 3 * * * root sa-learn --spam /var/mail/otherdomain.com/*/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin # ham: archive directories 15 3 * * * root sa-learn --ham /var/mail/otherdomain.com/*/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin # ham: inbox subdirectories 30 3 * * * root sa-learn --ham /var/mail/otherdomain.com/*/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin Then with plain docker-compose : services : mail : image : mailserver/docker-mailserver:latest volumes : - ./cron/sa-learn:/etc/cron.d/sa-learn Or with docker swarm : version : \"3.3\" services : mail : image : mailserver/docker-mailserver:latest # ... configs : - source : my_sa_crontab target : /etc/cron.d/sa-learn configs : my_sa_crontab : file : ./cron/sa-learn With the default settings, SpamAssassin will require 200 mails trained for spam (for example with the method explained above) and 200 mails trained for ham (using the same command as above but using --ham and providing it with some ham mails). Until you provided these 200+200 mails, SpamAssassin will not take the learned mails into account. For further reference, see the SpamAssassin Wiki . How can I configure a catch-all? Considering you want to redirect all incoming e-mails for the domain domain.tld to user1@domain.tld , add the following line to config/postfix-virtual.cf : @domain.tld user1@domain.tld How can I delete all the emails for a specific user? First of all, create a special alias named devnull by editing config/postfix-aliases.cf : devnull: /dev/null Considering you want to delete all the e-mails received for baduser@domain.tld , add the following line to config/postfix-virtual.cf : baduser@domain.tld devnull How do I have more control about what SPAMASSASIN is filtering? By default, SPAM and INFECTED emails are put to a quarantine which is not very straight forward to access. Several config settings are affecting this behavior: First, make sure you have the proper thresholds set: SA_TAG = -100000.0 SA_TAG2 = 3.75 SA_KILL = 100000.0 The very negative vaue in SA_TAG makes sure, that all emails have the SpamAssassin headers included. SA_TAG2 is the actual threshold to set the YES/NO flag for spam detection. SA_KILL needs to be very high, to make sure nothing is bounced at all ( SA_KILL superseeds SPAMASSASSIN_SPAM_TO_INBOX ) Make sure everything (including SPAM) is delivered to the inbox and not quarantined: SPAMASSASSIN_SPAM_TO_INBOX = 1 Use MOVE_SPAM_TO_JUNK=1 or create a sieve script which puts spam to the Junk folder: require [ \"comparator-i;ascii-numeric\" , \"relational\" , \"fileinto\" ]; if header :contains \"X-Spam-Flag\" \"YES\" { fileinto \"Junk\" ; } elsif allof ( not header :matches \"x-spam-score\" \"-*\" , header :value \"ge\" :comparator \"i;ascii-numeric\" \"x-spam-score\" \"3.75\" ) { fileinto \"Junk\" ; } Create a dedicated mailbox for emails which are infected/bad header and everything amavis is blocking by default and put its address into config/amavis.cf $clean_quarantine_to = \"amavis\\@domain.com\"; $virus_quarantine_to = \"amavis\\@domain.com\"; $banned_quarantine_to = \"amavis\\@domain.com\"; $bad_header_quarantine_to = \"amavis\\@domain.com\"; $spam_quarantine_to = \"amavis\\@domain.com\"; What kind of SSL certificates can I use? You can use the same certificates you use with another mail server. The only thing is that we provide a self-signed certificate tool and a letsencrypt certificate loader. I just moved from my old mail server, but \"it doesn't work\"? If this migration implies a DNS modification, be sure to wait for DNS propagation before opening an issue. Few examples of symptoms can be found here or here . This could be related to a modification of your MX record, or the IP mapped to mail.my-domain.tld . Additionally, validate your DNS configuration . If everything is OK regarding DNS, please provide formatted logs and config files. This will allow us to help you. If we're blind, we won't be able to do anything. What system requirements are required to run docker-mailserver effectively? 1 core and 1GB of RAM + swap partition is recommended to run docker-mailserver with clamav. Otherwise, it could work with 512M of RAM. Warning Clamav can consume a lot of memory, as it reads the entire signature database into RAM. Current figure is about 850M and growing. If you get errors about clamav or amavis failing to allocate memory you need more RAM or more swap and of course docker must be allowed to use swap (not always the case). If you can't use swap at all you may need 3G RAM. Can docker-mailserver run in a Rancher Environment? Yes, by adding the environment variable PERMIT_DOCKER: network . Warning Adding the docker network's gateway to the list of trusted hosts, e.g. using the network or connected-networks option, can create an open relay , for instance if IPv6 is enabled on the host machine but not in Docker . How can I Authenticate Users with SMTP_ONLY ? See #1247 for an example. Todo Write a How-to / Use-Case / Tutorial about authentication with SMTP_ONLY . Common Errors warning: connect to Milter service inet:localhost:8893: Connection refused # DMARC not running # = > /etc/init.d/opendmarc restart warning: connect to Milter service inet:localhost:8891: Connection refused # DKIM not running # = > /etc/init.d/opendkim restart mail amavis[1459]: (01459-01) (!)connect to /var/run/clamav/clamd.ctl failed, attempt #1: Can't connect to a UNIX socket /var/run/clamav/clamd.ctl: No such file or directory mail amavis[1459]: (01459-01) (!)ClamAV-clamd: All attempts (1) failed connecting to /var/run/clamav/clamd.ctl, retrying (2) mail amavis[1459]: (01459-01) (!)ClamAV-clamscan av-scanner FAILED: /usr/bin/clamscan KILLED, signal 9 (0009) at (eval 100) line 905. mail amavis[1459]: (01459-01) (!!)AV: ALL VIRUS SCANNERS FAILED # Clamav is not running ( not started or because you don ' t have enough memory ) # = > check requirements and/or start Clamav How to use when behind a Proxy Add to /etc/postfix/main.cf : proxy_interfaces = X.X.X.X (your public IP) What About Updates You can of course use a own script or every now and then pull && stop && rm && start the images but there are tools available for this. There is a section in the Update and Cleanup documentation page that explains how to use it the docker way. How to adjust settings with the user-patches.sh script Suppose you want to change a number of settings that are not listed as variables or add things to the server that are not included? This docker-container has a built-in way to do post-install processes. If you place a script called user-patches.sh in the config directory it will be run after all configuration files are set up, but before the postfix, amavis and other daemons are started. The config file I am talking about is this volume in the yml file: ./config/:/tmp/docker-mailserver/ To place such a script you can just make it in the config dir, for instance like this: cd ./config touch user-patches.sh chmod +x user-patches.sh Then fill user-patches.sh with suitable code. If you want to test it you can move into the running container, run it and see if it does what you want. For instance: # start shell in container ./setup.sh debug login # check the file cat /tmp/docker-mailserver/user-patches.sh # run the script /tmp/docker-mailserver/user-patches.sh # exit the container shell back to the host shell exit You can do a lot of things with such a script. You can find an example user-patches.sh script here: example user-patches.sh script Special use-case - Patching the supervisord config It seems worth noting, that the user-patches.sh gets executed trough supervisord. If you need to patch some supervisord config (e.g. /etc/supervisor/conf.d/saslauth.conf ), the patching happens too late. An easy workaround is to make the user-patches.sh reload the supervisord config after patching it: #!/bin/bash sed -i 's/rimap -r/rimap/' /etc/supervisor/conf.d/saslauth.conf supervisorctl update","title":"FAQ"},{"location":"faq/#what-kind-of-database-are-you-using","text":"None! No database is required. Filesystem is the database. This image is based on config files that can be persisted using Docker volumes, and as such versioned, backed up and so forth.","title":"What kind of database are you using?"},{"location":"faq/#where-are-emails-stored","text":"Mails are stored in /var/mail/${domain}/${username} . Since v9.0.0 it is possible to add custom user_attributes for each accounts to have a different mailbox configuration (See #1792 ). Warning You should use a data volume container for /var/mail to persist data. Otherwise, your data may be lost.","title":"Where are emails stored?"},{"location":"faq/#how-to-alter-the-running-mailserver-instance-without-relaunching-the-container","text":"docker-mailserver aggregates multiple \"sub-services\", such as Postfix, Dovecot, Fail2ban, SpamAssassin, etc. In many cases, one may edit a sub-service's config and reload that very sub-service, without stopping and relaunching the whole mail server. In order to do so, you'll probably want to push your config updates to your server through a Docker volume, then restart the sub-service to apply your changes, using supervisorctl . For instance, after editing fail2ban's config: supervisorctl restart fail2ban . See supervisorctl's documentation . Tip To add, update or delete an email account; there is no need to restart postfix / dovecot service inside the container after using setup.sh script. For more information, see #1639 .","title":"How to alter the running mailserver instance without relaunching the container?"},{"location":"faq/#how-can-i-sync-container-with-host-datetime-timezone","text":"Share the host's /etc/localtime with the docker-mailserver container, using a Docker volume: volumes : - /etc/localtime:/etc/localtime:ro Optional Add one line to .env or env-mailserver to set timetzone for container, for example: TZ = Europe/Berlin Check here for the tz name list","title":"How can I sync container with host date/time? Timezone?"},{"location":"faq/#what-is-the-file-format","text":"All files are using the Unix format with LF line endings. Please do not use CRLF .","title":"What is the file format?"},{"location":"faq/#what-about-backups","text":"Assuming that you use docker-compose and data volumes, you can backup the configuration, emails and logs like this: # create backup docker run --rm -ti \\ -v maildata:/var/mail \\ -v mailstate:/var/mail-state \\ -v maillogs:/var/logs/mail \\ -v \" $PWD /config\" :/tmp/docker-mailserver \\ -v /backup/mail:/backup \\ alpine:latest \\ tar czf \"/backup/mail- $( date +%F ) .tar.gz\" /var/mail /var/mail-state /var/logs/mail /tmp/docker-mailserver # delete backups older than 30 days find /backup/mail -type f -mtime +30 -delete","title":"What about backups?"},{"location":"faq/#what-about-mail-state-folder","text":"This folder consolidates all data generated by the server itself to persist when you upgrade. Example of data folder persisted: lib-amavis, lib-clamav, lib-fail2ban, lib-postfix, lib-postgrey, lib-spamassasin, lib-spamassassin, spool-postfix, ...","title":"What about mail-state folder?"},{"location":"faq/#how-can-i-configure-my-email-client","text":"Login are full email address ( user@domain.com ). # imap username : password : server : imap port : 143 or 993 with ssl (recommended) imap path prefix : INBOX # smtp smtp port : 25 or 587 with ssl (recommended) username : password : Please use STARTTLS .","title":"How can I configure my email client?"},{"location":"faq/#how-can-i-manage-my-custom-spamassassin-rules","text":"Antispam rules are managed in config/spamassassin-rules.cf .","title":"How can I manage my custom SpamAssassin rules?"},{"location":"faq/#what-are-acceptable-sa_spam_subject-values","text":"For no subject set SA_SPAM_SUBJECT=undef . For a trailing white-space subject one can define the whole variable with quotes in docker-compose.yml : environment : - \"SA_SPAM_SUBJECT=[SPAM] \"","title":"What are acceptable SA_SPAM_SUBJECT values?"},{"location":"faq/#can-i-use-nakedbare-domains-no-host-name","text":"Yes, but not without some configuration changes. Normally it is assumed that docker-mailserver runs on a host with a name, so the fully qualified host name might be mail.example.com with the domain example.com . The MX records point to mail.example.com . To use a bare domain where the host name is example.com and the domain is also example.com , change mydestination : From: mydestination = $myhostname, localhost.$mydomain, localhost To: mydestination = localhost.$mydomain, localhost Add the latter line to config/postfix-main.cf . That should work. Without that change there will be warnings in the logs like: warning: do not list domain example.com in BOTH mydestination and virtual_mailbox_domains Plus of course mail delivery fails.","title":"Can I use naked/bare domains (no host name)?"},{"location":"faq/#why-are-spamassassin-x-headers-not-inserted-into-my-sampledomaincom-subdomain-emails","text":"In the default setup, amavis only applies SpamAssassin x-headers into domains matching the template listed in the config file ( 05-domain_id in the amavis defaults). The default setup @local_domains_acl = ( \".$mydomain\" ); does not match subdomains. To match subdomains, you can override the @local_domains_acl directive in the amavis user config file 50-user with @local_domains_maps = (\".\"); to match any sort of domain template.","title":"Why are SpamAssassin x-headers not inserted into my sample.domain.com subdomain emails?"},{"location":"faq/#how-can-i-make-spamassassin-better-recognize-spam","text":"Put received spams in .Junk/ imap folder using SPAMASSASSIN_SPAM_TO_INBOX=1 and MOVE_SPAM_TO_JUNK=1 and add a user cron like the following: # This assumes you're having `environment: ONE_DIR=1` in the `mailserver.env`, # with a consolidated config in `/var/mail-state` # # m h dom mon dow command # Everyday 2:00AM, learn spam from a specific user 0 2 * * * docker exec mail sa-learn --spam /var/mail/domain.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin If you run the server with docker-compose , you can leverage on docker configs and the mailserver's own cron. This is less problematic than the simple solution shown above, because it decouples the learning from the host on which the mailserver is running and avoids errors if the server is not running. The following configuration works nicely: Example Create a system cron file: # in the docker-compose.yml root directory mkdir cron touch cron/sa-learn chown root:root cron/sa-learn chmod 0644 cron/sa-learn Edit the system cron file nano cron/sa-learn , and set an appropriate configuration: # This assumes you're having `environment: ONE_DIR=1` in the env-mailserver, # with a consolidated config in `/var/mail-state` # # m h dom mon dow user command # # Everyday 2:00AM, learn spam from a specific user # spam: junk directory 0 2 * * * root sa-learn --spam /var/mail/domain.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin # ham: archive directories 15 2 * * * root sa-learn --ham /var/mail/domain.com/username/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin # ham: inbox subdirectories 30 2 * * * root sa-learn --ham /var/mail/domain.com/username/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin # # Everyday 3:00AM, learn spam from all users of a domain # spam: junk directory 0 3 * * * root sa-learn --spam /var/mail/otherdomain.com/*/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin # ham: archive directories 15 3 * * * root sa-learn --ham /var/mail/otherdomain.com/*/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin # ham: inbox subdirectories 30 3 * * * root sa-learn --ham /var/mail/otherdomain.com/*/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin Then with plain docker-compose : services : mail : image : mailserver/docker-mailserver:latest volumes : - ./cron/sa-learn:/etc/cron.d/sa-learn Or with docker swarm : version : \"3.3\" services : mail : image : mailserver/docker-mailserver:latest # ... configs : - source : my_sa_crontab target : /etc/cron.d/sa-learn configs : my_sa_crontab : file : ./cron/sa-learn With the default settings, SpamAssassin will require 200 mails trained for spam (for example with the method explained above) and 200 mails trained for ham (using the same command as above but using --ham and providing it with some ham mails). Until you provided these 200+200 mails, SpamAssassin will not take the learned mails into account. For further reference, see the SpamAssassin Wiki .","title":"How can I make SpamAssassin better recognize spam?"},{"location":"faq/#how-can-i-configure-a-catch-all","text":"Considering you want to redirect all incoming e-mails for the domain domain.tld to user1@domain.tld , add the following line to config/postfix-virtual.cf : @domain.tld user1@domain.tld","title":"How can I configure a catch-all?"},{"location":"faq/#how-can-i-delete-all-the-emails-for-a-specific-user","text":"First of all, create a special alias named devnull by editing config/postfix-aliases.cf : devnull: /dev/null Considering you want to delete all the e-mails received for baduser@domain.tld , add the following line to config/postfix-virtual.cf : baduser@domain.tld devnull","title":"How can I delete all the emails for a specific user?"},{"location":"faq/#how-do-i-have-more-control-about-what-spamassasin-is-filtering","text":"By default, SPAM and INFECTED emails are put to a quarantine which is not very straight forward to access. Several config settings are affecting this behavior: First, make sure you have the proper thresholds set: SA_TAG = -100000.0 SA_TAG2 = 3.75 SA_KILL = 100000.0 The very negative vaue in SA_TAG makes sure, that all emails have the SpamAssassin headers included. SA_TAG2 is the actual threshold to set the YES/NO flag for spam detection. SA_KILL needs to be very high, to make sure nothing is bounced at all ( SA_KILL superseeds SPAMASSASSIN_SPAM_TO_INBOX ) Make sure everything (including SPAM) is delivered to the inbox and not quarantined: SPAMASSASSIN_SPAM_TO_INBOX = 1 Use MOVE_SPAM_TO_JUNK=1 or create a sieve script which puts spam to the Junk folder: require [ \"comparator-i;ascii-numeric\" , \"relational\" , \"fileinto\" ]; if header :contains \"X-Spam-Flag\" \"YES\" { fileinto \"Junk\" ; } elsif allof ( not header :matches \"x-spam-score\" \"-*\" , header :value \"ge\" :comparator \"i;ascii-numeric\" \"x-spam-score\" \"3.75\" ) { fileinto \"Junk\" ; } Create a dedicated mailbox for emails which are infected/bad header and everything amavis is blocking by default and put its address into config/amavis.cf $clean_quarantine_to = \"amavis\\@domain.com\"; $virus_quarantine_to = \"amavis\\@domain.com\"; $banned_quarantine_to = \"amavis\\@domain.com\"; $bad_header_quarantine_to = \"amavis\\@domain.com\"; $spam_quarantine_to = \"amavis\\@domain.com\";","title":"How do I have more control about what SPAMASSASIN is filtering?"},{"location":"faq/#what-kind-of-ssl-certificates-can-i-use","text":"You can use the same certificates you use with another mail server. The only thing is that we provide a self-signed certificate tool and a letsencrypt certificate loader.","title":"What kind of SSL certificates can I use?"},{"location":"faq/#i-just-moved-from-my-old-mail-server-but-it-doesnt-work","text":"If this migration implies a DNS modification, be sure to wait for DNS propagation before opening an issue. Few examples of symptoms can be found here or here . This could be related to a modification of your MX record, or the IP mapped to mail.my-domain.tld . Additionally, validate your DNS configuration . If everything is OK regarding DNS, please provide formatted logs and config files. This will allow us to help you. If we're blind, we won't be able to do anything.","title":"I just moved from my old mail server, but \"it doesn't work\"?"},{"location":"faq/#what-system-requirements-are-required-to-run-docker-mailserver-effectively","text":"1 core and 1GB of RAM + swap partition is recommended to run docker-mailserver with clamav. Otherwise, it could work with 512M of RAM. Warning Clamav can consume a lot of memory, as it reads the entire signature database into RAM. Current figure is about 850M and growing. If you get errors about clamav or amavis failing to allocate memory you need more RAM or more swap and of course docker must be allowed to use swap (not always the case). If you can't use swap at all you may need 3G RAM.","title":"What system requirements are required to run docker-mailserver effectively?"},{"location":"faq/#can-docker-mailserver-run-in-a-rancher-environment","text":"Yes, by adding the environment variable PERMIT_DOCKER: network . Warning Adding the docker network's gateway to the list of trusted hosts, e.g. using the network or connected-networks option, can create an open relay , for instance if IPv6 is enabled on the host machine but not in Docker .","title":"Can docker-mailserver run in a Rancher Environment?"},{"location":"faq/#how-can-i-authenticate-users-with-smtp_only","text":"See #1247 for an example. Todo Write a How-to / Use-Case / Tutorial about authentication with SMTP_ONLY .","title":"How can I Authenticate Users with SMTP_ONLY?"},{"location":"faq/#common-errors","text":"warning: connect to Milter service inet:localhost:8893: Connection refused # DMARC not running # = > /etc/init.d/opendmarc restart warning: connect to Milter service inet:localhost:8891: Connection refused # DKIM not running # = > /etc/init.d/opendkim restart mail amavis[1459]: (01459-01) (!)connect to /var/run/clamav/clamd.ctl failed, attempt #1: Can't connect to a UNIX socket /var/run/clamav/clamd.ctl: No such file or directory mail amavis[1459]: (01459-01) (!)ClamAV-clamd: All attempts (1) failed connecting to /var/run/clamav/clamd.ctl, retrying (2) mail amavis[1459]: (01459-01) (!)ClamAV-clamscan av-scanner FAILED: /usr/bin/clamscan KILLED, signal 9 (0009) at (eval 100) line 905. mail amavis[1459]: (01459-01) (!!)AV: ALL VIRUS SCANNERS FAILED # Clamav is not running ( not started or because you don ' t have enough memory ) # = > check requirements and/or start Clamav","title":"Common Errors"},{"location":"faq/#how-to-use-when-behind-a-proxy","text":"Add to /etc/postfix/main.cf : proxy_interfaces = X.X.X.X (your public IP)","title":"How to use when behind a Proxy"},{"location":"faq/#what-about-updates","text":"You can of course use a own script or every now and then pull && stop && rm && start the images but there are tools available for this. There is a section in the Update and Cleanup documentation page that explains how to use it the docker way.","title":"What About Updates"},{"location":"faq/#how-to-adjust-settings-with-the-user-patchessh-script","text":"Suppose you want to change a number of settings that are not listed as variables or add things to the server that are not included? This docker-container has a built-in way to do post-install processes. If you place a script called user-patches.sh in the config directory it will be run after all configuration files are set up, but before the postfix, amavis and other daemons are started. The config file I am talking about is this volume in the yml file: ./config/:/tmp/docker-mailserver/ To place such a script you can just make it in the config dir, for instance like this: cd ./config touch user-patches.sh chmod +x user-patches.sh Then fill user-patches.sh with suitable code. If you want to test it you can move into the running container, run it and see if it does what you want. For instance: # start shell in container ./setup.sh debug login # check the file cat /tmp/docker-mailserver/user-patches.sh # run the script /tmp/docker-mailserver/user-patches.sh # exit the container shell back to the host shell exit You can do a lot of things with such a script. You can find an example user-patches.sh script here: example user-patches.sh script","title":"How to adjust settings with the user-patches.sh script"},{"location":"faq/#special-use-case-patching-the-supervisord-config","text":"It seems worth noting, that the user-patches.sh gets executed trough supervisord. If you need to patch some supervisord config (e.g. /etc/supervisor/conf.d/saslauth.conf ), the patching happens too late. An easy workaround is to make the user-patches.sh reload the supervisord config after patching it: #!/bin/bash sed -i 's/rimap -r/rimap/' /etc/supervisor/conf.d/saslauth.conf supervisorctl update","title":"Special use-case - Patching the supervisord config"},{"location":"introduction/","text":"An Introduction to Mail Servers What is a mail server and how does it perform its duty? Here's an introduction to the field that covers everything you need to know to get started with docker-mailserver . Anatomy of a Mail Server A mail server is only a part of a client-server relationship aimed at exchanging information in the form of emails . Exchanging emails requires using specific means (programs and protocols). docker-mailserver provides you with the server portion, whereas the client can be anything from a terminal via text-based software (eg. Mutt ) to a fully-fledged desktop application (eg. Mozilla Thunderbird , Microsoft Outlook \u2026), to a web interface, etc. Unlike the client-side where usually a single program is used to perform retrieval and viewing of emails, the server-side is composed of many specialized components. The mail server is capable of accepting, forwarding, delivering, storing and overall exchanging messages, but each one of those tasks is actually handled by a specific piece of software. All of these \"agents\" must be integrated with one another for the exchange to take place. docker-mailserver has made informed choices about those components and their (default) configuration. It offers a comprehensive platform to run a fully featured mail server in no time! Components The following components are required to create a complete delivery chain : MUA: a Mail User Agent is basically any client/program capable of sending emails to arbitrary mail servers; while also capable of fetching emails from mail servers for presenting them to the end users. MTA: a Mail Transfer Agent is the so-called \"mail server\" as seen from the MUA's perspective. It's a piece of software dedicated to accepting submitted emails, then forwarding them-where exactly will depend on an email's final destination. If the receiving MTA is responsible for the hostname the email is sent to, then an MTA is to forward that email to an MDA (see below). Otherwise, it is to transfer (ie. forward, relay) to another MTA, \"closer\" to the email's final destination. MDA: a Mail Delivery Agent is responsible for accepting emails from an MTA and dropping them into their recipients' mailboxes, whichever the form. Here's a schematic view of mail delivery: Sending an email: MUA ----> MTA ----> (MTA relays) ----> MDA Fetching an email: MUA <--------------------------------- MDA There may be other moving parts or sub-divisions (for instance, at several points along the chain, specialized programs may be analyzing, filtering, bouncing, editing\u2026 the exchanged emails). In a nutshell, docker-mailserver provides you with the following components: A MTA: Postfix A MDA: Dovecot A bunch of additional programs to improve security and emails processing Here's where docker-mailserver 's toochain fits within the delivery chain: docker-mailserver is here: \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 Sending an email: MUA ---> MTA ---> (MTA relays) ---> \u252b MTA \u256e \u2503 Fetching an email: MUA <------------------------------ \u252b MDA \u256f \u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Example Let's say Alice owns a Gmail account, alice@gmail.com ; and Bob owns an account on a docker-mailserver 's instance, bob@dms.io . Make sure not to conflate these two very different scenarios: A) Alice sends an email to bob@dms.io => the email is first submitted to MTA smtp.gmail.com , then relayed to MTA smtp.dms.io where it is then delivered into Bob's mailbox. B) Bob sends an email to alice@gmail.com => the email is first submitted to MTA smtp.dms.io , then relayed to MTA smtp.gmail.com and eventually delivered into Alice's mailbox. In scenario A the email leaves Gmail's premises, that email's initial submission is not handled by your docker-mailserver instance(MTA); it merely receives the email after it has been relayed by Gmail's MTA. In scenario B , the docker-mailserver instance(MTA) handles the submission, prior to relaying. The main takeaway is that when a third-party sends an email to a docker-mailserver instance(MTA) (or any MTA for that matter), it does not establish a direct connection with that MTA. Email submission first goes through the sender's MTA, then some relaying between at least two MTAs is required to deliver the email. That will prove very important when it comes to security management. One important thing to note is that MTA and MDA programs may actually handle multiple tasks (which is the case with docker-mailserver 's Postfix and Dovecot). For instance, Postfix is both an SMTP server (accepting emails) and a relaying MTA (transferring, ie. sending emails to other MTA/MDA); Dovecot is both an MDA (delivering emails in mailboxes) and an IMAP server (allowing MUAs to fetch emails from the mail server ). On top of that, Postfix may rely on Dovecot's authentication capabilities. The exact relationship between all the components and their respective (sometimes shared) responsibilities is beyond the scope of this document. Please explore this wiki & the web to get more insights about docker-mailserver 's toolchain. About Security & Ports In the previous section, different components were outlined. Each one of those is responsible for a specific task, it has a specific purpose. Three main purposes exist when it comes to exchanging emails: Submission : for a MUA (client), the act of sending actual email data over the network, toward an MTA (server). Transfer (aka. Relay ): for an MTA, the act of sending actual email data over the network, toward another MTA (server) closer to the final destination (where an MTA will forward data to an MDA). Retrieval : for a MUA (client), the act of fetching actual email data over the network, from an MDA. Postfix handles Submission (and might handle Relay), whereas Dovecot handles Retrieval. They both need to be accessible by MUAs in order to act as servers, therefore they expose public endpoints on specific TCP ports (see. Understanding the ports for more details). Those endpoints may be secured, using an encryption scheme and TLS certificates. When it comes to the specifics of email exchange, we have to look at protocols and ports enabled to support all the identified purposes. There are several valid options and they've been evolving overtime. Here's docker-mailserver 's default configuration: Purpose Protocol TCP port / encryption Transfer/Relay SMTP 25 (unencrypted) Submission ESMTP 587 (encrypted using STARTTLS) Retrieval IMAP4 143 (encrypted using STARTTLS) + 993 (TLS) Retrieval POP3 Not activated \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Transfer/Relay \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510 MUA ----- STARTTLS ---> \u2524(587) MTA \u256e (25)\u251c <-- cleartext ---> \u250a Third-party MTA \u250a ---- cleartext ---> \u2524(25) \u2502 | \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518 |\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504| MUA <---- STARTTLS ---- \u2524(143) MDA \u256f | <-- enforced TLS -- \u2524(993) | \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Retrieval \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b If you're new to email infrastructure, both that table and the schema may be confusing. Read on to expand your understanding and learn about docker-mailserver 's configuration, including how you can customize it. Submission - SMTP For a MUA to send an email to an MTA, it needs to establish a connection with that server, then push data packets over a network that both the MUA (client) and the MTA (server) are connected to. The server implements the SMTP protocol, which makes it capable of handling Submission . In the case of docker-mailserver , the MTA (SMTP server) is Postfix. The MUA (client) may vary, yet its Submission request is performed as TCP packets sent over the public internet. This exchange of information may be secured in order to counter eavesdropping. Two kinds of Submission Let's say I own an account on a docker-mailserver instance, me@dms.io . There are two very different use-cases for Submission: I want to send an email to someone Someone wants to send you an email In the first scenario, I will be submitting my email directly to my docker-mailserver instance/MTA (Postfix), which will then relay the email to its recipient's MTA for final delivery. In this case, Submission is first handled by establishing a direct connection to my own MTA-so at least for this portion of the delivery chain, I'll be able to ensure security/confidentiality. Not so much for what comes next, ie. relaying between MTAs and final delivery. In the second scenario, a third-party email account owner will be first submitting an email to some third-party MTA. I have no control over this initial portion of the delivery chain, nor do I have control over the relaying that comes next. My MTA will merely accept a relayed email coming \"out of the blue\". My MTA will thus have to support two kinds of Submission: Outward Submission (self-owned email is submitted directly to the MTA, then is relayed \"outside\") Inward Submission (third-party email has been submitted & relayed, then is accepted \"inside\" by the MTA) \u250f\u2501\u2501\u2501\u2501 Outward Submission \u2501\u2501\u2501\u2501\u2513 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510 Me ---------------> \u2524 \u251c -----------------> \u250a \u250a \u2502 My MTA \u2502 \u250a Third-party MTA \u250a \u2502 \u251c <----------------- \u250a \u250a \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Inward Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Outward Submission The best practice as of 2020 when it comes to securing Outward Submission is to use Implicit TLS connection via ESMTP on port 465 (see RFC 8314 ). Let's break it down. Implicit TLS means the server enforces the client into using an encrypted TCP connection, using TLS . With this kind of connection, the MUA has to establish a TLS-encrypted connection from the get go (TLS is implied, hence the name \"Implicit\"). Any client attempting to either submit email in cleartext (unencrypted, not secure), or requesting a cleartext connection to be upgraded to a TLS-encrypted one using STARTTLS , is to be denied. Implicit TLS is sometimes called Enforced TLS for that reason. ESMTP is SMTP + extensions. It's the version of the SMTP protocol that most mail servers speak nowadays. For the purpose of this documentation, ESMTP and SMTP are synonymous. Port 465 is the reserved TCP port for Implicit TLS Submission (since 2018). There is actually a boisterous history to that ports usage, but let's keep it simple. Warning This Submission setup is sometimes refered to as SMTPS . Long story short: this is incorrect and should be avoided. Although a very satisfactory setup, Implicit TLS on port 465 is somewhat \"cutting edge\". There exists another well established mail Submission setup that must be supported as well, SMTP+STARTTLS on port 587. It uses Explicit TLS: the client starts with a cleartext connection, then the server informs a TLS-encrypted \"upgraded\" connection may be established, and the client may eventually decide to establish it prior to the Submission. Basically it's an opportunistic, opt-in TLS upgrade of the connection between the client and the server, at the client's discretion, using a mechanism known as STARTTLS that both ends need to implement. In many implementations, the mail server doesn't enforce TLS encryption, for backwards compatibility. Clients are thus free to deny the TLS-upgrade proposal (or misled by a hacker about STARTTLS not being available), and the server accepts unencrypted (cleartext) mail exchange, which poses a confidentiality threat and, to some extent, spam issues. RFC 8314 (section 3.3) recommends for mail servers to support both Implicit and Explicit TLS for Submission, and to enforce TLS-encryption on ports 587 (Explicit TLS) and 465 (Implicit TLS). That's exactly docker-mailserver 's default configuration: abiding by RFC 8314, it enforces a strict ( encrypt ) STARTTLS policy , where a denied TLS upgrade terminates the connection thus (hopefully but at the client's discretion) preventing unencrypted (cleartext) Submission. docker-mailserver 's default configuration enables and requires Explicit TLS (STARTTLS) on port 587 for Outward Submission. It does not enable Implicit TLS Outward Submission on port 465 by default. One may enable it through simple custom configuration, either as a replacement or (better!) supplementary mean of secure Submission. It does not support old MUAs (clients) not supporting TLS encryption on ports 587/465 (those should perform Submission on port 25, more details below). One may relax that constraint through advanced custom configuration, for backwards compatibility. A final Outward Submission setup exists and is akin SMTP+STARTTLS on port 587, but on port 25. That port has historically been reserved specifically for unencrypted (cleartext) mail exchange though, making STARTTLS a bit wrong to use. As is expected by RFC 5321 , docker-mailserver uses port 25 for unencrypted Submission in order to support older clients, but most importantly for unencrypted Transfer/Relay between MTAs. docker-mailserver 's default configuration also enables unencrypted (cleartext) on port 25 for Outward Submission. It does not enable Explicit TLS (STARTTLS) on port 25 by default. One may enable it through advanced custom configuration, either as a replacement (bad!) or as a supplementary mean of secure Outward Submission. One may also secure Outward Submission using advanced encryption scheme, such as DANE/DNSSEC and/or MTA-STS. Inward Submission Granted it's still very difficult enforcing encryption between MTAs (Transfer/Relay) without risking dropping emails (when relayed by MTAs not supporting TLS-encryption), Inward Submission is to be handled in cleartext on port 25 by default. docker-mailserver 's default configuration enables unencrypted (cleartext) on port 25 for Inward Submission. It does not enable Explicit TLS (STARTTLS) on port 25 by default. One may enable it through advanced custom configuration, either as a replacement (bad!) or as a supplementary mean of secure Inward Submission. One may also secure Inward Submission using advanced encryption scheme, such as DANE/DNSSEC and/or MTA-STS. Overall, docker-mailserver 's default configuration for SMTP looks like this: \u250f\u2501\u2501\u2501\u2501 Outward Submission \u2501\u2501\u2501\u2501\u2513 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510 Me -- cleartext --> \u2524(25) (25)\u251c --- cleartext ---> \u250a \u250a Me -- STARTTLS ---> \u2524(587) My MTA \u2502 \u250a Third-party MTA \u250a \u2502 (25)\u251c <---cleartext ---- \u250a \u250a \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Inward Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Retrieval - IMAP A MUA willing to fetch an email from a mail server will most likely communicate with its IMAP server. As with SMTP described earlier, communication will take place in the form of data packets exchanged over a network that both the client and the server are connected to. The IMAP protocol makes the server capable of handling Retrieval . In the case of docker-mailserver , the IMAP server is Dovecot. The MUA (client) may vary, yet its Retrieval request is performed as TCP packets sent over the public internet. This exchange of information may be secured in order to counter eavesdropping. Again, as with SMTP described earlier, the IMAP protocol may be secured with either Implicit TLS (aka. IMAPS / IMAP4S) or Explicit TLS (using STARTTLS). The best practice as of 2020 is to enforce IMAPS on port 993, rather than IMAP+STARTTLS on port 143 (see RFC 8314 ); yet the latter is usually provided for backwards compatibility. docker-mailserver 's default configuration enables both Implicit and Explicit TLS for Retrievial, on ports 993 and 143 respectively. Retrieval - POP3 Similarly to IMAP, the older POP3 protocol may be secured with either Implicit or Explicit TLS. The best practice as of 2020 would be POP3S on port 995, rather than POP3 +STARTTLS on port 110 (see RFC 8314 ). docker-mailserver 's default configuration disables POP3 altogether. One should expect MUAs to use TLS-encrypted IMAP for Retrieval. How does docker-mailserver help with setting everything up? As a batteries included Docker image, docker-mailserver provides you with all the required components and a default configuration, to run a decent and secure mail server. One may then customize all aspects of its internal components. Simple customization is supported through docker-compose configuration and the env-mailserver configuration file. Advanced customization is supported through providing \"monkey-patching\" configuration files and/or deriving your own image from docker-mailserver 's upstream, for a complete control over how things run. On the subject of security, one might consider docker-mailserver 's default configuration to not be 100% secure: it enables unencrypted traffic on port 25 it enables Explicit TLS (STARTTLS) on port 587, instead of Implicit TLS on port 465 We believe docker-mailserver 's default configuration to be a good middle ground: it goes slightly beyond \"old\" (1999) RFC 2487 ; and with developer friendly configuration settings, it makes it pretty easy to abide by the \"newest\" (2018) RFC 8314 . Eventually, it is up to you deciding exactly what kind of transportation/encryption to use and/or enforce, and to customize your instance accordingly (with looser or stricter security). Be also aware that protocols and ports on your server can only go so far with security; third-party MTAs might relay your emails on insecure connections, man-in-the-middle attacks might still prove effective, etc. Advanced counter-measure such as DANE, MTA-STS and/or full body encryption (eg. PGP) should be considered as well for increased confidentiality, but ideally without compromising backwards compatibility so as to not block emails. The README is the best starting point in configuring and running your mail server. You may then explore this wiki to cover additional topics, including but not limited to, security.","title":"Introduction"},{"location":"introduction/#an-introduction-to-mail-servers","text":"What is a mail server and how does it perform its duty? Here's an introduction to the field that covers everything you need to know to get started with docker-mailserver .","title":"An Introduction to Mail Servers"},{"location":"introduction/#anatomy-of-a-mail-server","text":"A mail server is only a part of a client-server relationship aimed at exchanging information in the form of emails . Exchanging emails requires using specific means (programs and protocols). docker-mailserver provides you with the server portion, whereas the client can be anything from a terminal via text-based software (eg. Mutt ) to a fully-fledged desktop application (eg. Mozilla Thunderbird , Microsoft Outlook \u2026), to a web interface, etc. Unlike the client-side where usually a single program is used to perform retrieval and viewing of emails, the server-side is composed of many specialized components. The mail server is capable of accepting, forwarding, delivering, storing and overall exchanging messages, but each one of those tasks is actually handled by a specific piece of software. All of these \"agents\" must be integrated with one another for the exchange to take place. docker-mailserver has made informed choices about those components and their (default) configuration. It offers a comprehensive platform to run a fully featured mail server in no time!","title":"Anatomy of a Mail Server"},{"location":"introduction/#components","text":"The following components are required to create a complete delivery chain : MUA: a Mail User Agent is basically any client/program capable of sending emails to arbitrary mail servers; while also capable of fetching emails from mail servers for presenting them to the end users. MTA: a Mail Transfer Agent is the so-called \"mail server\" as seen from the MUA's perspective. It's a piece of software dedicated to accepting submitted emails, then forwarding them-where exactly will depend on an email's final destination. If the receiving MTA is responsible for the hostname the email is sent to, then an MTA is to forward that email to an MDA (see below). Otherwise, it is to transfer (ie. forward, relay) to another MTA, \"closer\" to the email's final destination. MDA: a Mail Delivery Agent is responsible for accepting emails from an MTA and dropping them into their recipients' mailboxes, whichever the form. Here's a schematic view of mail delivery: Sending an email: MUA ----> MTA ----> (MTA relays) ----> MDA Fetching an email: MUA <--------------------------------- MDA There may be other moving parts or sub-divisions (for instance, at several points along the chain, specialized programs may be analyzing, filtering, bouncing, editing\u2026 the exchanged emails). In a nutshell, docker-mailserver provides you with the following components: A MTA: Postfix A MDA: Dovecot A bunch of additional programs to improve security and emails processing Here's where docker-mailserver 's toochain fits within the delivery chain: docker-mailserver is here: \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 Sending an email: MUA ---> MTA ---> (MTA relays) ---> \u252b MTA \u256e \u2503 Fetching an email: MUA <------------------------------ \u252b MDA \u256f \u2503 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b Example Let's say Alice owns a Gmail account, alice@gmail.com ; and Bob owns an account on a docker-mailserver 's instance, bob@dms.io . Make sure not to conflate these two very different scenarios: A) Alice sends an email to bob@dms.io => the email is first submitted to MTA smtp.gmail.com , then relayed to MTA smtp.dms.io where it is then delivered into Bob's mailbox. B) Bob sends an email to alice@gmail.com => the email is first submitted to MTA smtp.dms.io , then relayed to MTA smtp.gmail.com and eventually delivered into Alice's mailbox. In scenario A the email leaves Gmail's premises, that email's initial submission is not handled by your docker-mailserver instance(MTA); it merely receives the email after it has been relayed by Gmail's MTA. In scenario B , the docker-mailserver instance(MTA) handles the submission, prior to relaying. The main takeaway is that when a third-party sends an email to a docker-mailserver instance(MTA) (or any MTA for that matter), it does not establish a direct connection with that MTA. Email submission first goes through the sender's MTA, then some relaying between at least two MTAs is required to deliver the email. That will prove very important when it comes to security management. One important thing to note is that MTA and MDA programs may actually handle multiple tasks (which is the case with docker-mailserver 's Postfix and Dovecot). For instance, Postfix is both an SMTP server (accepting emails) and a relaying MTA (transferring, ie. sending emails to other MTA/MDA); Dovecot is both an MDA (delivering emails in mailboxes) and an IMAP server (allowing MUAs to fetch emails from the mail server ). On top of that, Postfix may rely on Dovecot's authentication capabilities. The exact relationship between all the components and their respective (sometimes shared) responsibilities is beyond the scope of this document. Please explore this wiki & the web to get more insights about docker-mailserver 's toolchain.","title":"Components"},{"location":"introduction/#about-security-ports","text":"In the previous section, different components were outlined. Each one of those is responsible for a specific task, it has a specific purpose. Three main purposes exist when it comes to exchanging emails: Submission : for a MUA (client), the act of sending actual email data over the network, toward an MTA (server). Transfer (aka. Relay ): for an MTA, the act of sending actual email data over the network, toward another MTA (server) closer to the final destination (where an MTA will forward data to an MDA). Retrieval : for a MUA (client), the act of fetching actual email data over the network, from an MDA. Postfix handles Submission (and might handle Relay), whereas Dovecot handles Retrieval. They both need to be accessible by MUAs in order to act as servers, therefore they expose public endpoints on specific TCP ports (see. Understanding the ports for more details). Those endpoints may be secured, using an encryption scheme and TLS certificates. When it comes to the specifics of email exchange, we have to look at protocols and ports enabled to support all the identified purposes. There are several valid options and they've been evolving overtime. Here's docker-mailserver 's default configuration: Purpose Protocol TCP port / encryption Transfer/Relay SMTP 25 (unencrypted) Submission ESMTP 587 (encrypted using STARTTLS) Retrieval IMAP4 143 (encrypted using STARTTLS) + 993 (TLS) Retrieval POP3 Not activated \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Transfer/Relay \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510 MUA ----- STARTTLS ---> \u2524(587) MTA \u256e (25)\u251c <-- cleartext ---> \u250a Third-party MTA \u250a ---- cleartext ---> \u2524(25) \u2502 | \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518 |\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504| MUA <---- STARTTLS ---- \u2524(143) MDA \u256f | <-- enforced TLS -- \u2524(993) | \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Retrieval \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b If you're new to email infrastructure, both that table and the schema may be confusing. Read on to expand your understanding and learn about docker-mailserver 's configuration, including how you can customize it.","title":"About Security & Ports"},{"location":"introduction/#submission-smtp","text":"For a MUA to send an email to an MTA, it needs to establish a connection with that server, then push data packets over a network that both the MUA (client) and the MTA (server) are connected to. The server implements the SMTP protocol, which makes it capable of handling Submission . In the case of docker-mailserver , the MTA (SMTP server) is Postfix. The MUA (client) may vary, yet its Submission request is performed as TCP packets sent over the public internet. This exchange of information may be secured in order to counter eavesdropping.","title":"Submission - SMTP"},{"location":"introduction/#two-kinds-of-submission","text":"Let's say I own an account on a docker-mailserver instance, me@dms.io . There are two very different use-cases for Submission: I want to send an email to someone Someone wants to send you an email In the first scenario, I will be submitting my email directly to my docker-mailserver instance/MTA (Postfix), which will then relay the email to its recipient's MTA for final delivery. In this case, Submission is first handled by establishing a direct connection to my own MTA-so at least for this portion of the delivery chain, I'll be able to ensure security/confidentiality. Not so much for what comes next, ie. relaying between MTAs and final delivery. In the second scenario, a third-party email account owner will be first submitting an email to some third-party MTA. I have no control over this initial portion of the delivery chain, nor do I have control over the relaying that comes next. My MTA will merely accept a relayed email coming \"out of the blue\". My MTA will thus have to support two kinds of Submission: Outward Submission (self-owned email is submitted directly to the MTA, then is relayed \"outside\") Inward Submission (third-party email has been submitted & relayed, then is accepted \"inside\" by the MTA) \u250f\u2501\u2501\u2501\u2501 Outward Submission \u2501\u2501\u2501\u2501\u2513 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510 Me ---------------> \u2524 \u251c -----------------> \u250a \u250a \u2502 My MTA \u2502 \u250a Third-party MTA \u250a \u2502 \u251c <----------------- \u250a \u250a \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Inward Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b","title":"Two kinds of Submission"},{"location":"introduction/#outward-submission","text":"The best practice as of 2020 when it comes to securing Outward Submission is to use Implicit TLS connection via ESMTP on port 465 (see RFC 8314 ). Let's break it down. Implicit TLS means the server enforces the client into using an encrypted TCP connection, using TLS . With this kind of connection, the MUA has to establish a TLS-encrypted connection from the get go (TLS is implied, hence the name \"Implicit\"). Any client attempting to either submit email in cleartext (unencrypted, not secure), or requesting a cleartext connection to be upgraded to a TLS-encrypted one using STARTTLS , is to be denied. Implicit TLS is sometimes called Enforced TLS for that reason. ESMTP is SMTP + extensions. It's the version of the SMTP protocol that most mail servers speak nowadays. For the purpose of this documentation, ESMTP and SMTP are synonymous. Port 465 is the reserved TCP port for Implicit TLS Submission (since 2018). There is actually a boisterous history to that ports usage, but let's keep it simple. Warning This Submission setup is sometimes refered to as SMTPS . Long story short: this is incorrect and should be avoided. Although a very satisfactory setup, Implicit TLS on port 465 is somewhat \"cutting edge\". There exists another well established mail Submission setup that must be supported as well, SMTP+STARTTLS on port 587. It uses Explicit TLS: the client starts with a cleartext connection, then the server informs a TLS-encrypted \"upgraded\" connection may be established, and the client may eventually decide to establish it prior to the Submission. Basically it's an opportunistic, opt-in TLS upgrade of the connection between the client and the server, at the client's discretion, using a mechanism known as STARTTLS that both ends need to implement. In many implementations, the mail server doesn't enforce TLS encryption, for backwards compatibility. Clients are thus free to deny the TLS-upgrade proposal (or misled by a hacker about STARTTLS not being available), and the server accepts unencrypted (cleartext) mail exchange, which poses a confidentiality threat and, to some extent, spam issues. RFC 8314 (section 3.3) recommends for mail servers to support both Implicit and Explicit TLS for Submission, and to enforce TLS-encryption on ports 587 (Explicit TLS) and 465 (Implicit TLS). That's exactly docker-mailserver 's default configuration: abiding by RFC 8314, it enforces a strict ( encrypt ) STARTTLS policy , where a denied TLS upgrade terminates the connection thus (hopefully but at the client's discretion) preventing unencrypted (cleartext) Submission. docker-mailserver 's default configuration enables and requires Explicit TLS (STARTTLS) on port 587 for Outward Submission. It does not enable Implicit TLS Outward Submission on port 465 by default. One may enable it through simple custom configuration, either as a replacement or (better!) supplementary mean of secure Submission. It does not support old MUAs (clients) not supporting TLS encryption on ports 587/465 (those should perform Submission on port 25, more details below). One may relax that constraint through advanced custom configuration, for backwards compatibility. A final Outward Submission setup exists and is akin SMTP+STARTTLS on port 587, but on port 25. That port has historically been reserved specifically for unencrypted (cleartext) mail exchange though, making STARTTLS a bit wrong to use. As is expected by RFC 5321 , docker-mailserver uses port 25 for unencrypted Submission in order to support older clients, but most importantly for unencrypted Transfer/Relay between MTAs. docker-mailserver 's default configuration also enables unencrypted (cleartext) on port 25 for Outward Submission. It does not enable Explicit TLS (STARTTLS) on port 25 by default. One may enable it through advanced custom configuration, either as a replacement (bad!) or as a supplementary mean of secure Outward Submission. One may also secure Outward Submission using advanced encryption scheme, such as DANE/DNSSEC and/or MTA-STS.","title":"Outward Submission"},{"location":"introduction/#inward-submission","text":"Granted it's still very difficult enforcing encryption between MTAs (Transfer/Relay) without risking dropping emails (when relayed by MTAs not supporting TLS-encryption), Inward Submission is to be handled in cleartext on port 25 by default. docker-mailserver 's default configuration enables unencrypted (cleartext) on port 25 for Inward Submission. It does not enable Explicit TLS (STARTTLS) on port 25 by default. One may enable it through advanced custom configuration, either as a replacement (bad!) or as a supplementary mean of secure Inward Submission. One may also secure Inward Submission using advanced encryption scheme, such as DANE/DNSSEC and/or MTA-STS. Overall, docker-mailserver 's default configuration for SMTP looks like this: \u250f\u2501\u2501\u2501\u2501 Outward Submission \u2501\u2501\u2501\u2501\u2513 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2510 Me -- cleartext --> \u2524(25) (25)\u251c --- cleartext ---> \u250a \u250a Me -- STARTTLS ---> \u2524(587) My MTA \u2502 \u250a Third-party MTA \u250a \u2502 (25)\u251c <---cleartext ---- \u250a \u250a \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518 \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Inward Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b","title":"Inward Submission"},{"location":"introduction/#retrieval-imap","text":"A MUA willing to fetch an email from a mail server will most likely communicate with its IMAP server. As with SMTP described earlier, communication will take place in the form of data packets exchanged over a network that both the client and the server are connected to. The IMAP protocol makes the server capable of handling Retrieval . In the case of docker-mailserver , the IMAP server is Dovecot. The MUA (client) may vary, yet its Retrieval request is performed as TCP packets sent over the public internet. This exchange of information may be secured in order to counter eavesdropping. Again, as with SMTP described earlier, the IMAP protocol may be secured with either Implicit TLS (aka. IMAPS / IMAP4S) or Explicit TLS (using STARTTLS). The best practice as of 2020 is to enforce IMAPS on port 993, rather than IMAP+STARTTLS on port 143 (see RFC 8314 ); yet the latter is usually provided for backwards compatibility. docker-mailserver 's default configuration enables both Implicit and Explicit TLS for Retrievial, on ports 993 and 143 respectively.","title":"Retrieval - IMAP"},{"location":"introduction/#retrieval-pop3","text":"Similarly to IMAP, the older POP3 protocol may be secured with either Implicit or Explicit TLS. The best practice as of 2020 would be POP3S on port 995, rather than POP3 +STARTTLS on port 110 (see RFC 8314 ). docker-mailserver 's default configuration disables POP3 altogether. One should expect MUAs to use TLS-encrypted IMAP for Retrieval.","title":"Retrieval - POP3"},{"location":"introduction/#how-does-docker-mailserver-help-with-setting-everything-up","text":"As a batteries included Docker image, docker-mailserver provides you with all the required components and a default configuration, to run a decent and secure mail server. One may then customize all aspects of its internal components. Simple customization is supported through docker-compose configuration and the env-mailserver configuration file. Advanced customization is supported through providing \"monkey-patching\" configuration files and/or deriving your own image from docker-mailserver 's upstream, for a complete control over how things run. On the subject of security, one might consider docker-mailserver 's default configuration to not be 100% secure: it enables unencrypted traffic on port 25 it enables Explicit TLS (STARTTLS) on port 587, instead of Implicit TLS on port 465 We believe docker-mailserver 's default configuration to be a good middle ground: it goes slightly beyond \"old\" (1999) RFC 2487 ; and with developer friendly configuration settings, it makes it pretty easy to abide by the \"newest\" (2018) RFC 8314 . Eventually, it is up to you deciding exactly what kind of transportation/encryption to use and/or enforce, and to customize your instance accordingly (with looser or stricter security). Be also aware that protocols and ports on your server can only go so far with security; third-party MTAs might relay your emails on insecure connections, man-in-the-middle attacks might still prove effective, etc. Advanced counter-measure such as DANE, MTA-STS and/or full body encryption (eg. PGP) should be considered as well for increased confidentiality, but ideally without compromising backwards compatibility so as to not block emails. The README is the best starting point in configuring and running your mail server. You may then explore this wiki to cover additional topics, including but not limited to, security.","title":"How does docker-mailserver help with setting everything up?"},{"location":"config/environment/","text":"Info Values in bold are the default values. If an option doesn't work as documented here, check if you are running the latest image. The current master branch corresponds to the image mailserver/docker-mailserver:edge . General OVERRIDE_HOSTNAME empty => uses the hostname command to get the mail server's canonical hostname. => Specify a fully-qualified domainname to serve mail for. This is used for many of the config features so if you can't set your hostname (e.g. you're in a container platform that doesn't let you) specify it in this environment variable. DMS_DEBUG 0 => Debug disabled 1 => Enables debug on startup SUPERVISOR_LOGLEVEL Here you can adjust the log-level for Supervisor . Possible values are critical => Only show critical messages error => Only show erroneous output warn => Show warnings info => Normal informational output debug => Also show debug messages The log-level will show everything in its class and above. ONE_DIR 0 => state in default directories. 1 => consolidate all states into a single directory ( /var/mail-state ) to allow persistence using docker volumes. PERMIT_DOCKER Set different options for mynetworks option (can be overwrite in postfix-main.cf) WARNING : Adding the docker network's gateway to the list of trusted hosts, e.g. using the network or connected-networks option, can create an open relay , for instance if IPv6 is enabled on the host machine but not in Docker. empty => localhost only. host => Add docker host (ipv4 only). network => Add the docker default bridge network (172.16.0.0/12); WARNING : docker-compose might use others (e.g. 192.168.0.0/16) use PERMIT_DOCKER=connected-networks in this case. connected-networks => Add all connected docker networks (ipv4 only). Note: you probably want to set POSTFIX_INET_PROTOCOLS=ipv4 to make it work fine with Docker. ENABLE_AMAVIS Amavis content filter (used for ClamAV & SpamAssassin) 0 => Amavis is disabled 1 => Amavis is enabled AMAVIS_LOGLEVEL This page provides information on Amavis' logging statistics. -1/-2/-3 => Only show errors 0 => Show warnings 1/2 => Show default informational output 3/4/5 => log debug information (very verbose) ENABLE_CLAMAV 0 => Clamav is disabled 1 => Clamav is enabled ENABLE_POP3 empty => POP3 service disabled 1 => Enables POP3 service ENABLE_FAIL2BAN 0 => fail2ban service disabled 1 => Enables fail2ban service If you enable Fail2Ban, don't forget to add the following lines to your docker-compose.yml : cap_add: - NET_ADMIN Otherwise, iptables won't be able to ban IPs. FAIL2BAN_BLOCKTYPE drop => drop packet (send NO reply) reject => reject packet (send ICMP unreachable) FAIL2BAN_BLOCKTYPE=drop SMTP_ONLY empty => all daemons start 1 => only launch postfix smtp SSL_TYPE empty => SSL disabled. letsencrypt => Enables Let's Encrypt certificates. custom => Enables custom certificates. manual => Let you manually specify locations of your SSL certificates for non-standard cases Requires: SSL_CERT_PATH and SSL_KEY_PATH ENV vars to be set to the location of the files within the container. Optional: SSL_ALT_CERT_PATH and SSL_ALT_KEY_PATH allow providing a 2nd certificate as a fallback for dual (aka hybrid) certificate support. Useful for ECDSA with an RSA fallback. Presently only manual mode supports this feature. self-signed => Enables self-signed certificates. Please read the SSL page in the documentation for more information. TLS_LEVEL empty => modern modern => Enables TLSv1.2 and modern ciphers only. (default) intermediate => Enables TLSv1, TLSv1.1 and TLSv1.2 and broad compatibility ciphers. SPOOF_PROTECTION Configures the handling of creating mails with forged sender addresses. empty => Mail address spoofing allowed. Any logged in user may create email messages with a forged sender address. See also Wikipedia (not recommended, but default for backwards compatibility reasons) 1 => (recommended) Mail spoofing denied. Each user may only send with his own or his alias addresses. Addresses with extension delimiters are not able to send messages. ENABLE_SRS Enables the Sender Rewriting Scheme. SRS is needed if your mail server acts as forwarder. See postsrsd for further explanation. 0 => Disabled 1 => Enabled NETWORK_INTERFACE In case your network interface differs from eth0 , e.g. when you are using HostNetworking in Kubernetes, you can set this to whatever interface you want. This interface will then be used. empty => eth0 VIRUSMAILS_DELETE_DELAY Set how many days a virusmail will stay on the server before being deleted empty => 7 days ENABLE_POSTFIX_VIRTUAL_TRANSPORT This Option is activating the Usage of POSTFIX_DAGENT to specify a ltmp client different from default dovecot socket. empty => disabled 1 => enabled POSTFIX_DAGENT Enabled by ENABLE_POSTFIX_VIRTUAL_TRANSPORT. Specify the final delivery of postfix empty : fail lmtp:unix:private/dovecot-lmtp (use socket) lmtps:inet:: (secure lmtp with starttls, take a look at https://sys4.de/en/blog/2014/11/17/sicheres-lmtp-mit-starttls-in-dovecot/ ) lmtp::2003 (use kopano as mailstore) etc. POSTFIX_MAILBOX_SIZE_LIMIT Set the mailbox size limit for all users. If set to zero, the size will be unlimited (default). empty => 0 (no limit) ENABLE_QUOTAS 1 => Dovecot quota is enabled 0 => Dovecot quota is disabled See mailbox quota . POSTFIX_MESSAGE_SIZE_LIMIT Set the message size limit for all users. If set to zero, the size will be unlimited (not recommended!) empty => 10240000 (~10 MB) ENABLE_MANAGESIEVE empty => Managesieve service disabled 1 => Enables Managesieve on port 4190 OVERRIDE_HOSTNAME empty => uses the hostname command to get the mail server's canonical hostname => Specify a fully-qualified domainname to serve mail for. This is used for many of the config features so if you can't set your hostname (e.g. you're in a container platform that doesn't let you) specify it in this environment variable. POSTMASTER_ADDRESS empty => postmaster@domain.com => Specify the postmaster address ENABLE_UPDATE_CHECK Check for updates on container start and then once a day. If an update is available, a mail is send to POSTMASTER_ADDRESS. 0 => Update check disabled 1 => Update check enabled UPDATE_CHECK_INTERVAL Customize the update check interval. Number + Suffix. Suffix must be 's' for seconds, 'm' for minutes, 'h' for hours or 'd' for days. 1d => Check for updates once a day POSTSCREEN_ACTION enforce => Allow other tests to complete. Reject attempts to deliver mail with a 550 SMTP reply, and log the helo/sender/recipient information. Repeat this test the next time the client connects. drop => Drop the connection immediately with a 521 SMTP reply. Repeat this test the next time the client connects. ignore => Ignore the failure of this test. Allow other tests to complete. Repeat this test the next time the client connects. This option is useful for testing and collecting statistics without blocking mail. DOVECOT_MAILBOX_FORMAT maildir => uses very common Maildir format, one file contains one message sdbox => (experimental) uses Dovecot high-performance mailbox format, one file contains one message mdbox ==> (experimental) uses Dovecot high-performance mailbox format, multiple messages per file and multiple files per box This option has been added in November 2019. Using other format than Maildir is considered as experimental in docker-mailserver and should only be used for testing purpose. For more details, please refer to Dovecot Documentation . POSTFIX_INET_PROTOCOLS all => All possible protocols. ipv4 => Use only IPv4 traffic. Most likely you want this behind Docker. ipv6 => Use only IPv6 traffic. Note: More details in http://www.postfix.org/postconf.5.html#inet_protocols Reports PFLOGSUMM_TRIGGER Enables regular pflogsumm mail reports. not set => No report daily_cron => Daily report for the previous day logrotate => Full report based on the mail log when it is rotated This is a new option. The old REPORT options are still supported for backwards compatibility. If this is not set and reports are enabled with the old options, logrotate will be used. PFLOGSUMM_RECIPIENT Recipient address for pflogsumm reports. not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS => Specify the recipient address(es) PFLOGSUMM_SENDER From address for pflogsumm reports. not set => Use REPORT_SENDER or POSTMASTER_ADDRESS => Specify the sender address LOGWATCH_INTERVAL Interval for logwatch report. none => No report is generated daily => Send a daily report weekly => Send a report every week LOGWATCH_RECIPIENT Recipient address for logwatch reports if they are enabled. not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS => Specify the recipient address(es) REPORT_RECIPIENT (deprecated) Enables a report being sent (created by pflogsumm) on a regular basis. 0 => Report emails are disabled unless enabled by other options 1 => Using POSTMASTER_ADDRESS as the recipient => Specify the recipient address REPORT_SENDER (deprecated) Change the sending address for mail report empty => mailserver-report@hostname => Specify the report sender (From) address REPORT_INTERVAL (deprecated) Changes the interval in which logs are rotated and a report is being sent (deprecated). daily => Send a daily report weekly => Send a report every week monthly => Send a report every month Note: This variable used to control logrotate inside the container and sent the pflogsumm report when the logs were rotated. It is still supported for backwards compatibility, but the new option LOGROTATE_INTERVAL has been added that only rotates the logs. LOGROTATE_INTERVAL Defines the interval in which the mail log is being rotated. daily => Rotate daily. weekly => Rotate weekly. monthly => Rotate monthly. Note that only the log inside the container is affected. The full log output is still available via docker logs mail (or your respective container name). If you want to control logrotation for the docker generated logfile see: Docker Logging Drivers . Also note that by default the logs are lost when the container is recycled. To keep the logs, mount a volume. Finally the logrotate interval may affect the period for generated reports. That is the case when the reports are triggered by log rotation. SpamAssassin ENABLE_SPAMASSASSIN 0 => SpamAssassin is disabled 1 => SpamAssassin is enabled /!\\ Spam delivery: when SpamAssassin is enabled, messages marked as spam WILL NOT BE DELIVERED. Use SPAMASSASSIN_SPAM_TO_INBOX=1 for receiving spam messages. SPAMASSASSIN_SPAM_TO_INBOX 0 => Spam messages will be bounced ( rejected ) without any notification ( dangerous ). 1 => Spam messages will be delivered to the inbox and tagged as spam using SA_SPAM_SUBJECT . MOVE_SPAM_TO_JUNK 1 => Spam messages will be delivered in the Junk folder. 0 => Spam messages will be delivered in the mailbox. Note: this setting needs SPAMASSASSIN_SPAM_TO_INBOX=1 SA_TAG 2.0 => add spam info headers if at, or above that level Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1 SA_TAG2 6.31 => add 'spam detected' headers at that level Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1 SA_KILL 6.31 => triggers spam evasive actions Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1 . By default, the mailserver is configured to quarantine spam emails. If emails are quarantined, they are compressed and stored in a location dependent on the ONE_DIR setting above. If ONE_DIR=1 the location is /var/mail-state/lib-amavis/virusmails/. If ONE_DIR=0 it is /var/lib/amavis/virusmails/. These paths are inside the docker container. To inhibit this behaviour and deliver spam emails, set this to a very high value e.g. 100.0. SA_SPAM_SUBJECT ***SPAM*** => add tag to subject if spam detected Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1 . Add the SpamAssassin score to the subject line by inserting the keyword _SCORE_: ***SPAM(_SCORE_)*** . SA_SHORTCIRCUIT_BAYES_SPAM 1 => will activate SpamAssassin short circuiting for bayes spam detection. This will uncomment the respective line in /etc/spamassasin/local.cf Note: activate this only if you are confident in your bayes database for identifying spam. SA_SHORTCIRCUIT_BAYES_HAM 1 => will activate SpamAssassin short circuiting for bayes ham detection This will uncomment the respective line in /etc/spamassasin/local.cf Note: activate this only if you are confident in your bayes database for identifying ham. Fetchmail ENABLE_FETCHMAIL 0 => fetchmail disabled 1 => fetchmail enabled FETCHMAIL_POLL 300 => fetchmail The number of seconds for the interval FETCHMAIL_PARALLEL 0 => fetchmail runs with a single config file /etc/fetchmailrc 1 => /etc/fetchmailrc is split per poll entry. For every poll entry a seperate fetchmail instance is started to allow having multiple imap idle configurations defined. Note: The defaults of your fetchmailrc file need to be at the top of the file. Otherwise it won't be added correctly to all separate fetchmail instances. LDAP ENABLE_LDAP empty => LDAP authentification is disabled 1 => LDAP authentification is enabled NOTE: A second container for the ldap service is necessary (e.g. docker-openldap ) For preparing the ldap server to use in combination with this container this article may be helpful LDAP_START_TLS empty => no yes => LDAP over TLS enabled for Postfix LDAP_SERVER_HOST empty => mail.domain.com => Specify the dns-name/ip-address where the ldap-server is listening, or an URI like ldaps://mail.domain.com NOTE: If you going to use the mailserver in combination with docker-compose you can set the service name here LDAP_SEARCH_BASE empty => ou=people,dc=domain,dc=com => e.g. LDAP_SEARCH_BASE=dc=mydomain,dc=local LDAP_BIND_DN empty => cn=admin,dc=domain,dc=com => take a look at examples of SASL_LDAP_BIND_DN LDAP_BIND_PW empty => admin => Specify the password to bind against ldap LDAP_QUERY_FILTER_USER e.g. (&(mail=%s)(mailEnabled=TRUE)) => Specify how ldap should be asked for users LDAP_QUERY_FILTER_GROUP e.g. (&(mailGroupMember=%s)(mailEnabled=TRUE)) => Specify how ldap should be asked for groups LDAP_QUERY_FILTER_ALIAS e.g. (&(mailAlias=%s)(mailEnabled=TRUE)) => Specify how ldap should be asked for aliases LDAP_QUERY_FILTER_DOMAIN e.g. (&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE)) => Specify how ldap should be asked for domains LDAP_QUERY_FILTER_SENDERS empty => use user/alias/group maps directly, equivalent to (|($LDAP_QUERY_FILTER_USER)($LDAP_QUERY_FILTER_ALIAS)($LDAP_QUERY_FILTER_GROUP)) => Override how ldap should be asked if a sender address is allowed for a user DOVECOT_TLS empty => no yes => LDAP over TLS enabled for Dovecot Dovecot The following variables overwrite the default values for /etc/dovecot/dovecot-ldap.conf.ext . DOVECOT_BASE empty => same as LDAP_SEARCH_BASE => Tell Dovecot to search only below this base entry. (e.g. ou=people,dc=domain,dc=com ) DOVECOT_DEFAULT_PASS_SCHEME empty => SSHA => Select one crypt scheme for password hashing from this list of password schemes . DOVECOT_DN empty => same as LDAP_BIND_DN => Bind dn for LDAP connection. (e.g. cn=admin,dc=domain,dc=com ) DOVECOT_DNPASS empty => same as LDAP_BIND_PW => Password for LDAP dn sepecifified in DOVECOT_DN . DOVECOT_URIS empty => same as LDAP_SERVER_HOST => Specify a space separated list of LDAP uris. Note: If the protocol is missing, ldap:// will be used. Note: This deprecates DOVECOT_HOSTS (as it didn't allow to use LDAPS), which is currently still supported for backwards compatibility. DOVECOT_LDAP_VERSION empty => 3 2 => LDAP version 2 is used 3 => LDAP version 3 is used DOVECOT_AUTH_BIND empty => no yes => Enable LDAP authentication binds DOVECOT_USER_FILTER e.g. (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) DOVECOT_USER_ATTRS e.g. homeDirectory=home,qmailUID=uid,qmailGID=gid,mailMessageStore=mail => Specify the directory to dovecot attribute mapping that fits your directory structure. Note: This is necessary for directories that do not use the Postfix Book Schema. Note: The left-hand value is the directory attribute, the right hand value is the dovecot variable. More details on the Dovecot Wiki DOVECOT_PASS_FILTER e.g. (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) empty => same as DOVECOT_USER_FILTER DOVECOT_PASS_ATTRS e.g. uid=user,userPassword=password => Specify the directory to dovecot variable mapping that fits your directory structure. Note: This is necessary for directories that do not use the Postfix Book Schema. Note: The left-hand value is the directory attribute, the right hand value is the dovecot variable. More details on the Dovecot Wiki Postgrey ENABLE_POSTGREY 0 => postgrey is disabled 1 => postgrey is enabled POSTGREY_DELAY 300 => greylist for N seconds Note: This postgrey setting needs ENABLE_POSTGREY=1 POSTGREY_MAX_AGE 35 => delete entries older than N days since the last time that they have been seen Note: This postgrey setting needs ENABLE_POSTGREY=1 POSTGREY_AUTO_WHITELIST_CLIENTS 5 => whitelist host after N successful deliveries (N=0 to disable whitelisting) Note: This postgrey setting needs ENABLE_POSTGREY=1 POSTGREY_TEXT Delayed by Postgrey => response when a mail is greylisted Note: This postgrey setting needs ENABLE_POSTGREY=1 SASL Auth ENABLE_SASLAUTHD 0 => saslauthd is disabled 1 => saslauthd is enabled SASLAUTHD_MECHANISMS empty => pam ldap => authenticate against ldap server shadow => authenticate against local user db mysql => authenticate against mysql db rimap => authenticate against imap server NOTE: can be a list of mechanisms like pam ldap shadow SASLAUTHD_MECH_OPTIONS empty => None e.g. with SASLAUTHD_MECHANISMS rimap you need to specify the ip-address/servername of the imap server ==> xxx.xxx.xxx.xxx SASLAUTHD_LDAP_SERVER empty => same as LDAP_SERVER_HOST Note: since version 10.0.0, you can specify a protocol here (like ldaps://); this deprecates SASLAUTHD_LDAP_SSL. SASLAUTHD_LDAP_START_TLS empty => no yes => Enable ldap_start_tls option SASLAUTHD_LDAP_TLS_CHECK_PEER empty => no yes => Enable ldap_tls_check_peer option SASLAUTHD_LDAP_TLS_CACERT_DIR Path to directory with CA (Certificate Authority) certificates. empty => Nothing is added to the configuration Any value => Fills the ldap_tls_cacert_dir option SASLAUTHD_LDAP_TLS_CACERT_FILE File containing CA (Certificate Authority) certificate(s). empty => Nothing is added to the configuration Any value => Fills the ldap_tls_cacert_file option SASLAUTHD_LDAP_BIND_DN empty => same as LDAP_BIND_DN specify an object with privileges to search the directory tree e.g. active directory: SASLAUTHD_LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=net e.g. openldap: SASLAUTHD_LDAP_BIND_DN=cn=admin,dc=mydomain,dc=net SASLAUTHD_LDAP_PASSWORD empty => same as LDAP_BIND_PW SASLAUTHD_LDAP_SEARCH_BASE empty => same as LDAP_SEARCH_BASE specify the search base SASLAUTHD_LDAP_FILTER empty => default filter (&(uniqueIdentifier=%u)(mailEnabled=TRUE)) e.g. for active directory: (&(sAMAccountName=%U)(objectClass=person)) e.g. for openldap: (&(uid=%U)(objectClass=person)) SASLAUTHD_LDAP_PASSWORD_ATTR Specify what password attribute to use for password verification. empty => Nothing is added to the configuration but the documentation says it is userPassword by default. Any value => Fills the ldap_password_attr option SASL_PASSWD empty => No sasl_passwd will be created string => /etc/postfix/sasl_passwd will be created with the string as password SASLAUTHD_LDAP_AUTH_METHOD empty => bind will be used as a default value fastbind => The fastbind method is used custom => The custom method uses userPassword attribute to verify the password SASLAUTHD_LDAP_MECH Specify the authentication mechanism for SASL bind. empty => Nothing is added to the configuration Any value => Fills the ldap_mech option SRS (Sender Rewriting Scheme) SRS_SENDER_CLASSES An email has an \"envelope\" sender (indicating the sending server) and a \"header\" sender (indicating who sent it). More strict SPF policies may require you to replace both instead of just the envelope sender. More info . envelope_sender => Rewrite only envelope sender address header_sender => Rewrite only header sender (not recommended) envelope_sender,header_sender => Rewrite both senders SRS_EXCLUDE_DOMAINS empty => Envelope sender will be rewritten for all domains provide comma separated list of domains to exclude from rewriting SRS_SECRET empty => generated when the container is started for the first time provide a secret to use in base64 you may specify multiple keys, comma separated. the first one is used for signing and the remaining will be used for verification. this is how you rotate and expire keys if you have a cluster/swarm make sure the same keys are on all nodes example command to generate a key: dd if=/dev/urandom bs=24 count=1 2>/dev/null | base64 SRS_DOMAINNAME empty => Derived from OVERRIDE_HOSTNAME, DOMAINNAME, or the container's hostname Set this if auto-detection fails, isn't what you want, or you wish to have a separate container handle DSNs Default Relay Host DEFAULT_RELAY_HOST empty => don't set default relayhost setting in main.cf default host and port to relay all mail through. Format: [example.com]:587 (don't forget the brackets if you need this to be compatible with $RELAY_USER and $RELAY_PASSWORD , explained below). Multi-domain Relay Hosts RELAY_HOST empty => don't configure relay host default host to relay mail through RELAY_PORT empty => 25 default port to relay mail through RELAY_USER empty => no default default relay username (if no specific entry exists in postfix-sasl-password.cf) RELAY_PASSWORD empty => no default password for default relay user","title":"Environment Variables"},{"location":"config/environment/#general","text":"","title":"General"},{"location":"config/environment/#override_hostname","text":"empty => uses the hostname command to get the mail server's canonical hostname. => Specify a fully-qualified domainname to serve mail for. This is used for many of the config features so if you can't set your hostname (e.g. you're in a container platform that doesn't let you) specify it in this environment variable.","title":"OVERRIDE_HOSTNAME"},{"location":"config/environment/#dms_debug","text":"0 => Debug disabled 1 => Enables debug on startup","title":"DMS_DEBUG"},{"location":"config/environment/#supervisor_loglevel","text":"Here you can adjust the log-level for Supervisor . Possible values are critical => Only show critical messages error => Only show erroneous output warn => Show warnings info => Normal informational output debug => Also show debug messages The log-level will show everything in its class and above.","title":"SUPERVISOR_LOGLEVEL"},{"location":"config/environment/#one_dir","text":"0 => state in default directories. 1 => consolidate all states into a single directory ( /var/mail-state ) to allow persistence using docker volumes.","title":"ONE_DIR"},{"location":"config/environment/#permit_docker","text":"Set different options for mynetworks option (can be overwrite in postfix-main.cf) WARNING : Adding the docker network's gateway to the list of trusted hosts, e.g. using the network or connected-networks option, can create an open relay , for instance if IPv6 is enabled on the host machine but not in Docker. empty => localhost only. host => Add docker host (ipv4 only). network => Add the docker default bridge network (172.16.0.0/12); WARNING : docker-compose might use others (e.g. 192.168.0.0/16) use PERMIT_DOCKER=connected-networks in this case. connected-networks => Add all connected docker networks (ipv4 only). Note: you probably want to set POSTFIX_INET_PROTOCOLS=ipv4 to make it work fine with Docker.","title":"PERMIT_DOCKER"},{"location":"config/environment/#enable_amavis","text":"Amavis content filter (used for ClamAV & SpamAssassin) 0 => Amavis is disabled 1 => Amavis is enabled","title":"ENABLE_AMAVIS"},{"location":"config/environment/#amavis_loglevel","text":"This page provides information on Amavis' logging statistics. -1/-2/-3 => Only show errors 0 => Show warnings 1/2 => Show default informational output 3/4/5 => log debug information (very verbose)","title":"AMAVIS_LOGLEVEL"},{"location":"config/environment/#enable_clamav","text":"0 => Clamav is disabled 1 => Clamav is enabled","title":"ENABLE_CLAMAV"},{"location":"config/environment/#enable_pop3","text":"empty => POP3 service disabled 1 => Enables POP3 service","title":"ENABLE_POP3"},{"location":"config/environment/#enable_fail2ban","text":"0 => fail2ban service disabled 1 => Enables fail2ban service If you enable Fail2Ban, don't forget to add the following lines to your docker-compose.yml : cap_add: - NET_ADMIN Otherwise, iptables won't be able to ban IPs.","title":"ENABLE_FAIL2BAN"},{"location":"config/environment/#fail2ban_blocktype","text":"drop => drop packet (send NO reply) reject => reject packet (send ICMP unreachable) FAIL2BAN_BLOCKTYPE=drop","title":"FAIL2BAN_BLOCKTYPE"},{"location":"config/environment/#smtp_only","text":"empty => all daemons start 1 => only launch postfix smtp","title":"SMTP_ONLY"},{"location":"config/environment/#ssl_type","text":"empty => SSL disabled. letsencrypt => Enables Let's Encrypt certificates. custom => Enables custom certificates. manual => Let you manually specify locations of your SSL certificates for non-standard cases Requires: SSL_CERT_PATH and SSL_KEY_PATH ENV vars to be set to the location of the files within the container. Optional: SSL_ALT_CERT_PATH and SSL_ALT_KEY_PATH allow providing a 2nd certificate as a fallback for dual (aka hybrid) certificate support. Useful for ECDSA with an RSA fallback. Presently only manual mode supports this feature. self-signed => Enables self-signed certificates. Please read the SSL page in the documentation for more information.","title":"SSL_TYPE"},{"location":"config/environment/#tls_level","text":"empty => modern modern => Enables TLSv1.2 and modern ciphers only. (default) intermediate => Enables TLSv1, TLSv1.1 and TLSv1.2 and broad compatibility ciphers.","title":"TLS_LEVEL"},{"location":"config/environment/#spoof_protection","text":"Configures the handling of creating mails with forged sender addresses. empty => Mail address spoofing allowed. Any logged in user may create email messages with a forged sender address. See also Wikipedia (not recommended, but default for backwards compatibility reasons) 1 => (recommended) Mail spoofing denied. Each user may only send with his own or his alias addresses. Addresses with extension delimiters are not able to send messages.","title":"SPOOF_PROTECTION"},{"location":"config/environment/#enable_srs","text":"Enables the Sender Rewriting Scheme. SRS is needed if your mail server acts as forwarder. See postsrsd for further explanation. 0 => Disabled 1 => Enabled","title":"ENABLE_SRS"},{"location":"config/environment/#network_interface","text":"In case your network interface differs from eth0 , e.g. when you are using HostNetworking in Kubernetes, you can set this to whatever interface you want. This interface will then be used. empty => eth0","title":"NETWORK_INTERFACE"},{"location":"config/environment/#virusmails_delete_delay","text":"Set how many days a virusmail will stay on the server before being deleted empty => 7 days","title":"VIRUSMAILS_DELETE_DELAY"},{"location":"config/environment/#enable_postfix_virtual_transport","text":"This Option is activating the Usage of POSTFIX_DAGENT to specify a ltmp client different from default dovecot socket. empty => disabled 1 => enabled","title":"ENABLE_POSTFIX_VIRTUAL_TRANSPORT"},{"location":"config/environment/#postfix_dagent","text":"Enabled by ENABLE_POSTFIX_VIRTUAL_TRANSPORT. Specify the final delivery of postfix empty : fail lmtp:unix:private/dovecot-lmtp (use socket) lmtps:inet:: (secure lmtp with starttls, take a look at https://sys4.de/en/blog/2014/11/17/sicheres-lmtp-mit-starttls-in-dovecot/ ) lmtp::2003 (use kopano as mailstore) etc.","title":"POSTFIX_DAGENT"},{"location":"config/environment/#postfix_mailbox_size_limit","text":"Set the mailbox size limit for all users. If set to zero, the size will be unlimited (default). empty => 0 (no limit)","title":"POSTFIX_MAILBOX_SIZE_LIMIT"},{"location":"config/environment/#enable_quotas","text":"1 => Dovecot quota is enabled 0 => Dovecot quota is disabled See mailbox quota .","title":"ENABLE_QUOTAS"},{"location":"config/environment/#postfix_message_size_limit","text":"Set the message size limit for all users. If set to zero, the size will be unlimited (not recommended!) empty => 10240000 (~10 MB)","title":"POSTFIX_MESSAGE_SIZE_LIMIT"},{"location":"config/environment/#enable_managesieve","text":"empty => Managesieve service disabled 1 => Enables Managesieve on port 4190","title":"ENABLE_MANAGESIEVE"},{"location":"config/environment/#override_hostname_1","text":"empty => uses the hostname command to get the mail server's canonical hostname => Specify a fully-qualified domainname to serve mail for. This is used for many of the config features so if you can't set your hostname (e.g. you're in a container platform that doesn't let you) specify it in this environment variable.","title":"OVERRIDE_HOSTNAME"},{"location":"config/environment/#postmaster_address","text":"empty => postmaster@domain.com => Specify the postmaster address","title":"POSTMASTER_ADDRESS"},{"location":"config/environment/#enable_update_check","text":"Check for updates on container start and then once a day. If an update is available, a mail is send to POSTMASTER_ADDRESS. 0 => Update check disabled 1 => Update check enabled","title":"ENABLE_UPDATE_CHECK"},{"location":"config/environment/#update_check_interval","text":"Customize the update check interval. Number + Suffix. Suffix must be 's' for seconds, 'm' for minutes, 'h' for hours or 'd' for days. 1d => Check for updates once a day","title":"UPDATE_CHECK_INTERVAL"},{"location":"config/environment/#postscreen_action","text":"enforce => Allow other tests to complete. Reject attempts to deliver mail with a 550 SMTP reply, and log the helo/sender/recipient information. Repeat this test the next time the client connects. drop => Drop the connection immediately with a 521 SMTP reply. Repeat this test the next time the client connects. ignore => Ignore the failure of this test. Allow other tests to complete. Repeat this test the next time the client connects. This option is useful for testing and collecting statistics without blocking mail.","title":"POSTSCREEN_ACTION"},{"location":"config/environment/#dovecot_mailbox_format","text":"maildir => uses very common Maildir format, one file contains one message sdbox => (experimental) uses Dovecot high-performance mailbox format, one file contains one message mdbox ==> (experimental) uses Dovecot high-performance mailbox format, multiple messages per file and multiple files per box This option has been added in November 2019. Using other format than Maildir is considered as experimental in docker-mailserver and should only be used for testing purpose. For more details, please refer to Dovecot Documentation .","title":"DOVECOT_MAILBOX_FORMAT"},{"location":"config/environment/#postfix_inet_protocols","text":"all => All possible protocols. ipv4 => Use only IPv4 traffic. Most likely you want this behind Docker. ipv6 => Use only IPv6 traffic. Note: More details in http://www.postfix.org/postconf.5.html#inet_protocols","title":"POSTFIX_INET_PROTOCOLS"},{"location":"config/environment/#reports","text":"","title":"Reports"},{"location":"config/environment/#pflogsumm_trigger","text":"Enables regular pflogsumm mail reports. not set => No report daily_cron => Daily report for the previous day logrotate => Full report based on the mail log when it is rotated This is a new option. The old REPORT options are still supported for backwards compatibility. If this is not set and reports are enabled with the old options, logrotate will be used.","title":"PFLOGSUMM_TRIGGER"},{"location":"config/environment/#pflogsumm_recipient","text":"Recipient address for pflogsumm reports. not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS => Specify the recipient address(es)","title":"PFLOGSUMM_RECIPIENT"},{"location":"config/environment/#pflogsumm_sender","text":"From address for pflogsumm reports. not set => Use REPORT_SENDER or POSTMASTER_ADDRESS => Specify the sender address","title":"PFLOGSUMM_SENDER"},{"location":"config/environment/#logwatch_interval","text":"Interval for logwatch report. none => No report is generated daily => Send a daily report weekly => Send a report every week","title":"LOGWATCH_INTERVAL"},{"location":"config/environment/#logwatch_recipient","text":"Recipient address for logwatch reports if they are enabled. not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS => Specify the recipient address(es)","title":"LOGWATCH_RECIPIENT"},{"location":"config/environment/#report_recipient-deprecated","text":"Enables a report being sent (created by pflogsumm) on a regular basis. 0 => Report emails are disabled unless enabled by other options 1 => Using POSTMASTER_ADDRESS as the recipient => Specify the recipient address","title":"REPORT_RECIPIENT (deprecated)"},{"location":"config/environment/#report_sender-deprecated","text":"Change the sending address for mail report empty => mailserver-report@hostname => Specify the report sender (From) address","title":"REPORT_SENDER (deprecated)"},{"location":"config/environment/#report_interval-deprecated","text":"Changes the interval in which logs are rotated and a report is being sent (deprecated). daily => Send a daily report weekly => Send a report every week monthly => Send a report every month Note: This variable used to control logrotate inside the container and sent the pflogsumm report when the logs were rotated. It is still supported for backwards compatibility, but the new option LOGROTATE_INTERVAL has been added that only rotates the logs.","title":"REPORT_INTERVAL (deprecated)"},{"location":"config/environment/#logrotate_interval","text":"Defines the interval in which the mail log is being rotated. daily => Rotate daily. weekly => Rotate weekly. monthly => Rotate monthly. Note that only the log inside the container is affected. The full log output is still available via docker logs mail (or your respective container name). If you want to control logrotation for the docker generated logfile see: Docker Logging Drivers . Also note that by default the logs are lost when the container is recycled. To keep the logs, mount a volume. Finally the logrotate interval may affect the period for generated reports. That is the case when the reports are triggered by log rotation.","title":"LOGROTATE_INTERVAL"},{"location":"config/environment/#spamassassin","text":"","title":"SpamAssassin"},{"location":"config/environment/#enable_spamassassin","text":"0 => SpamAssassin is disabled 1 => SpamAssassin is enabled /!\\ Spam delivery: when SpamAssassin is enabled, messages marked as spam WILL NOT BE DELIVERED. Use SPAMASSASSIN_SPAM_TO_INBOX=1 for receiving spam messages.","title":"ENABLE_SPAMASSASSIN"},{"location":"config/environment/#spamassassin_spam_to_inbox","text":"0 => Spam messages will be bounced ( rejected ) without any notification ( dangerous ). 1 => Spam messages will be delivered to the inbox and tagged as spam using SA_SPAM_SUBJECT .","title":"SPAMASSASSIN_SPAM_TO_INBOX"},{"location":"config/environment/#move_spam_to_junk","text":"1 => Spam messages will be delivered in the Junk folder. 0 => Spam messages will be delivered in the mailbox. Note: this setting needs SPAMASSASSIN_SPAM_TO_INBOX=1","title":"MOVE_SPAM_TO_JUNK"},{"location":"config/environment/#sa_tag","text":"2.0 => add spam info headers if at, or above that level Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1","title":"SA_TAG"},{"location":"config/environment/#sa_tag2","text":"6.31 => add 'spam detected' headers at that level Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1","title":"SA_TAG2"},{"location":"config/environment/#sa_kill","text":"6.31 => triggers spam evasive actions Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1 . By default, the mailserver is configured to quarantine spam emails. If emails are quarantined, they are compressed and stored in a location dependent on the ONE_DIR setting above. If ONE_DIR=1 the location is /var/mail-state/lib-amavis/virusmails/. If ONE_DIR=0 it is /var/lib/amavis/virusmails/. These paths are inside the docker container. To inhibit this behaviour and deliver spam emails, set this to a very high value e.g. 100.0.","title":"SA_KILL"},{"location":"config/environment/#sa_spam_subject","text":"***SPAM*** => add tag to subject if spam detected Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1 . Add the SpamAssassin score to the subject line by inserting the keyword _SCORE_: ***SPAM(_SCORE_)*** .","title":"SA_SPAM_SUBJECT"},{"location":"config/environment/#sa_shortcircuit_bayes_spam","text":"1 => will activate SpamAssassin short circuiting for bayes spam detection. This will uncomment the respective line in /etc/spamassasin/local.cf Note: activate this only if you are confident in your bayes database for identifying spam.","title":"SA_SHORTCIRCUIT_BAYES_SPAM"},{"location":"config/environment/#sa_shortcircuit_bayes_ham","text":"1 => will activate SpamAssassin short circuiting for bayes ham detection This will uncomment the respective line in /etc/spamassasin/local.cf Note: activate this only if you are confident in your bayes database for identifying ham.","title":"SA_SHORTCIRCUIT_BAYES_HAM"},{"location":"config/environment/#fetchmail","text":"","title":"Fetchmail"},{"location":"config/environment/#enable_fetchmail","text":"0 => fetchmail disabled 1 => fetchmail enabled","title":"ENABLE_FETCHMAIL"},{"location":"config/environment/#fetchmail_poll","text":"300 => fetchmail The number of seconds for the interval","title":"FETCHMAIL_POLL"},{"location":"config/environment/#fetchmail_parallel","text":"0 => fetchmail runs with a single config file /etc/fetchmailrc 1 => /etc/fetchmailrc is split per poll entry. For every poll entry a seperate fetchmail instance is started to allow having multiple imap idle configurations defined. Note: The defaults of your fetchmailrc file need to be at the top of the file. Otherwise it won't be added correctly to all separate fetchmail instances.","title":"FETCHMAIL_PARALLEL"},{"location":"config/environment/#ldap","text":"","title":"LDAP"},{"location":"config/environment/#enable_ldap","text":"empty => LDAP authentification is disabled 1 => LDAP authentification is enabled NOTE: A second container for the ldap service is necessary (e.g. docker-openldap ) For preparing the ldap server to use in combination with this container this article may be helpful","title":"ENABLE_LDAP"},{"location":"config/environment/#ldap_start_tls","text":"empty => no yes => LDAP over TLS enabled for Postfix","title":"LDAP_START_TLS"},{"location":"config/environment/#ldap_server_host","text":"empty => mail.domain.com => Specify the dns-name/ip-address where the ldap-server is listening, or an URI like ldaps://mail.domain.com NOTE: If you going to use the mailserver in combination with docker-compose you can set the service name here","title":"LDAP_SERVER_HOST"},{"location":"config/environment/#ldap_search_base","text":"empty => ou=people,dc=domain,dc=com => e.g. LDAP_SEARCH_BASE=dc=mydomain,dc=local","title":"LDAP_SEARCH_BASE"},{"location":"config/environment/#ldap_bind_dn","text":"empty => cn=admin,dc=domain,dc=com => take a look at examples of SASL_LDAP_BIND_DN","title":"LDAP_BIND_DN"},{"location":"config/environment/#ldap_bind_pw","text":"empty => admin => Specify the password to bind against ldap","title":"LDAP_BIND_PW"},{"location":"config/environment/#ldap_query_filter_user","text":"e.g. (&(mail=%s)(mailEnabled=TRUE)) => Specify how ldap should be asked for users","title":"LDAP_QUERY_FILTER_USER"},{"location":"config/environment/#ldap_query_filter_group","text":"e.g. (&(mailGroupMember=%s)(mailEnabled=TRUE)) => Specify how ldap should be asked for groups","title":"LDAP_QUERY_FILTER_GROUP"},{"location":"config/environment/#ldap_query_filter_alias","text":"e.g. (&(mailAlias=%s)(mailEnabled=TRUE)) => Specify how ldap should be asked for aliases","title":"LDAP_QUERY_FILTER_ALIAS"},{"location":"config/environment/#ldap_query_filter_domain","text":"e.g. (&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE)) => Specify how ldap should be asked for domains","title":"LDAP_QUERY_FILTER_DOMAIN"},{"location":"config/environment/#ldap_query_filter_senders","text":"empty => use user/alias/group maps directly, equivalent to (|($LDAP_QUERY_FILTER_USER)($LDAP_QUERY_FILTER_ALIAS)($LDAP_QUERY_FILTER_GROUP)) => Override how ldap should be asked if a sender address is allowed for a user","title":"LDAP_QUERY_FILTER_SENDERS"},{"location":"config/environment/#dovecot_tls","text":"empty => no yes => LDAP over TLS enabled for Dovecot","title":"DOVECOT_TLS"},{"location":"config/environment/#dovecot","text":"The following variables overwrite the default values for /etc/dovecot/dovecot-ldap.conf.ext .","title":"Dovecot"},{"location":"config/environment/#dovecot_base","text":"empty => same as LDAP_SEARCH_BASE => Tell Dovecot to search only below this base entry. (e.g. ou=people,dc=domain,dc=com )","title":"DOVECOT_BASE"},{"location":"config/environment/#dovecot_default_pass_scheme","text":"empty => SSHA => Select one crypt scheme for password hashing from this list of password schemes .","title":"DOVECOT_DEFAULT_PASS_SCHEME"},{"location":"config/environment/#dovecot_dn","text":"empty => same as LDAP_BIND_DN => Bind dn for LDAP connection. (e.g. cn=admin,dc=domain,dc=com )","title":"DOVECOT_DN"},{"location":"config/environment/#dovecot_dnpass","text":"empty => same as LDAP_BIND_PW => Password for LDAP dn sepecifified in DOVECOT_DN .","title":"DOVECOT_DNPASS"},{"location":"config/environment/#dovecot_uris","text":"empty => same as LDAP_SERVER_HOST => Specify a space separated list of LDAP uris. Note: If the protocol is missing, ldap:// will be used. Note: This deprecates DOVECOT_HOSTS (as it didn't allow to use LDAPS), which is currently still supported for backwards compatibility.","title":"DOVECOT_URIS"},{"location":"config/environment/#dovecot_ldap_version","text":"empty => 3 2 => LDAP version 2 is used 3 => LDAP version 3 is used","title":"DOVECOT_LDAP_VERSION"},{"location":"config/environment/#dovecot_auth_bind","text":"empty => no yes => Enable LDAP authentication binds","title":"DOVECOT_AUTH_BIND"},{"location":"config/environment/#dovecot_user_filter","text":"e.g. (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))","title":"DOVECOT_USER_FILTER"},{"location":"config/environment/#dovecot_user_attrs","text":"e.g. homeDirectory=home,qmailUID=uid,qmailGID=gid,mailMessageStore=mail => Specify the directory to dovecot attribute mapping that fits your directory structure. Note: This is necessary for directories that do not use the Postfix Book Schema. Note: The left-hand value is the directory attribute, the right hand value is the dovecot variable. More details on the Dovecot Wiki","title":"DOVECOT_USER_ATTRS"},{"location":"config/environment/#dovecot_pass_filter","text":"e.g. (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) empty => same as DOVECOT_USER_FILTER","title":"DOVECOT_PASS_FILTER"},{"location":"config/environment/#dovecot_pass_attrs","text":"e.g. uid=user,userPassword=password => Specify the directory to dovecot variable mapping that fits your directory structure. Note: This is necessary for directories that do not use the Postfix Book Schema. Note: The left-hand value is the directory attribute, the right hand value is the dovecot variable. More details on the Dovecot Wiki","title":"DOVECOT_PASS_ATTRS"},{"location":"config/environment/#postgrey","text":"","title":"Postgrey"},{"location":"config/environment/#enable_postgrey","text":"0 => postgrey is disabled 1 => postgrey is enabled","title":"ENABLE_POSTGREY"},{"location":"config/environment/#postgrey_delay","text":"300 => greylist for N seconds Note: This postgrey setting needs ENABLE_POSTGREY=1","title":"POSTGREY_DELAY"},{"location":"config/environment/#postgrey_max_age","text":"35 => delete entries older than N days since the last time that they have been seen Note: This postgrey setting needs ENABLE_POSTGREY=1","title":"POSTGREY_MAX_AGE"},{"location":"config/environment/#postgrey_auto_whitelist_clients","text":"5 => whitelist host after N successful deliveries (N=0 to disable whitelisting) Note: This postgrey setting needs ENABLE_POSTGREY=1","title":"POSTGREY_AUTO_WHITELIST_CLIENTS"},{"location":"config/environment/#postgrey_text","text":"Delayed by Postgrey => response when a mail is greylisted Note: This postgrey setting needs ENABLE_POSTGREY=1","title":"POSTGREY_TEXT"},{"location":"config/environment/#sasl-auth","text":"","title":"SASL Auth"},{"location":"config/environment/#enable_saslauthd","text":"0 => saslauthd is disabled 1 => saslauthd is enabled","title":"ENABLE_SASLAUTHD"},{"location":"config/environment/#saslauthd_mechanisms","text":"empty => pam ldap => authenticate against ldap server shadow => authenticate against local user db mysql => authenticate against mysql db rimap => authenticate against imap server NOTE: can be a list of mechanisms like pam ldap shadow","title":"SASLAUTHD_MECHANISMS"},{"location":"config/environment/#saslauthd_mech_options","text":"empty => None e.g. with SASLAUTHD_MECHANISMS rimap you need to specify the ip-address/servername of the imap server ==> xxx.xxx.xxx.xxx","title":"SASLAUTHD_MECH_OPTIONS"},{"location":"config/environment/#saslauthd_ldap_server","text":"empty => same as LDAP_SERVER_HOST Note: since version 10.0.0, you can specify a protocol here (like ldaps://); this deprecates SASLAUTHD_LDAP_SSL.","title":"SASLAUTHD_LDAP_SERVER"},{"location":"config/environment/#saslauthd_ldap_start_tls","text":"empty => no yes => Enable ldap_start_tls option","title":"SASLAUTHD_LDAP_START_TLS"},{"location":"config/environment/#saslauthd_ldap_tls_check_peer","text":"empty => no yes => Enable ldap_tls_check_peer option","title":"SASLAUTHD_LDAP_TLS_CHECK_PEER"},{"location":"config/environment/#saslauthd_ldap_tls_cacert_dir","text":"Path to directory with CA (Certificate Authority) certificates. empty => Nothing is added to the configuration Any value => Fills the ldap_tls_cacert_dir option","title":"SASLAUTHD_LDAP_TLS_CACERT_DIR"},{"location":"config/environment/#saslauthd_ldap_tls_cacert_file","text":"File containing CA (Certificate Authority) certificate(s). empty => Nothing is added to the configuration Any value => Fills the ldap_tls_cacert_file option","title":"SASLAUTHD_LDAP_TLS_CACERT_FILE"},{"location":"config/environment/#saslauthd_ldap_bind_dn","text":"empty => same as LDAP_BIND_DN specify an object with privileges to search the directory tree e.g. active directory: SASLAUTHD_LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=net e.g. openldap: SASLAUTHD_LDAP_BIND_DN=cn=admin,dc=mydomain,dc=net","title":"SASLAUTHD_LDAP_BIND_DN"},{"location":"config/environment/#saslauthd_ldap_password","text":"empty => same as LDAP_BIND_PW","title":"SASLAUTHD_LDAP_PASSWORD"},{"location":"config/environment/#saslauthd_ldap_search_base","text":"empty => same as LDAP_SEARCH_BASE specify the search base","title":"SASLAUTHD_LDAP_SEARCH_BASE"},{"location":"config/environment/#saslauthd_ldap_filter","text":"empty => default filter (&(uniqueIdentifier=%u)(mailEnabled=TRUE)) e.g. for active directory: (&(sAMAccountName=%U)(objectClass=person)) e.g. for openldap: (&(uid=%U)(objectClass=person))","title":"SASLAUTHD_LDAP_FILTER"},{"location":"config/environment/#saslauthd_ldap_password_attr","text":"Specify what password attribute to use for password verification. empty => Nothing is added to the configuration but the documentation says it is userPassword by default. Any value => Fills the ldap_password_attr option","title":"SASLAUTHD_LDAP_PASSWORD_ATTR"},{"location":"config/environment/#sasl_passwd","text":"empty => No sasl_passwd will be created string => /etc/postfix/sasl_passwd will be created with the string as password","title":"SASL_PASSWD"},{"location":"config/environment/#saslauthd_ldap_auth_method","text":"empty => bind will be used as a default value fastbind => The fastbind method is used custom => The custom method uses userPassword attribute to verify the password","title":"SASLAUTHD_LDAP_AUTH_METHOD"},{"location":"config/environment/#saslauthd_ldap_mech","text":"Specify the authentication mechanism for SASL bind. empty => Nothing is added to the configuration Any value => Fills the ldap_mech option","title":"SASLAUTHD_LDAP_MECH"},{"location":"config/environment/#srs-sender-rewriting-scheme","text":"","title":"SRS (Sender Rewriting Scheme)"},{"location":"config/environment/#srs_sender_classes","text":"An email has an \"envelope\" sender (indicating the sending server) and a \"header\" sender (indicating who sent it). More strict SPF policies may require you to replace both instead of just the envelope sender. More info . envelope_sender => Rewrite only envelope sender address header_sender => Rewrite only header sender (not recommended) envelope_sender,header_sender => Rewrite both senders","title":"SRS_SENDER_CLASSES"},{"location":"config/environment/#srs_exclude_domains","text":"empty => Envelope sender will be rewritten for all domains provide comma separated list of domains to exclude from rewriting","title":"SRS_EXCLUDE_DOMAINS"},{"location":"config/environment/#srs_secret","text":"empty => generated when the container is started for the first time provide a secret to use in base64 you may specify multiple keys, comma separated. the first one is used for signing and the remaining will be used for verification. this is how you rotate and expire keys if you have a cluster/swarm make sure the same keys are on all nodes example command to generate a key: dd if=/dev/urandom bs=24 count=1 2>/dev/null | base64","title":"SRS_SECRET"},{"location":"config/environment/#srs_domainname","text":"empty => Derived from OVERRIDE_HOSTNAME, DOMAINNAME, or the container's hostname Set this if auto-detection fails, isn't what you want, or you wish to have a separate container handle DSNs","title":"SRS_DOMAINNAME"},{"location":"config/environment/#default-relay-host","text":"","title":"Default Relay Host"},{"location":"config/environment/#default_relay_host","text":"empty => don't set default relayhost setting in main.cf default host and port to relay all mail through. Format: [example.com]:587 (don't forget the brackets if you need this to be compatible with $RELAY_USER and $RELAY_PASSWORD , explained below).","title":"DEFAULT_RELAY_HOST"},{"location":"config/environment/#multi-domain-relay-hosts","text":"","title":"Multi-domain Relay Hosts"},{"location":"config/environment/#relay_host","text":"empty => don't configure relay host default host to relay mail through","title":"RELAY_HOST"},{"location":"config/environment/#relay_port","text":"empty => 25 default port to relay mail through","title":"RELAY_PORT"},{"location":"config/environment/#relay_user","text":"empty => no default default relay username (if no specific entry exists in postfix-sasl-password.cf)","title":"RELAY_USER"},{"location":"config/environment/#relay_password","text":"empty => no default password for default relay user","title":"RELAY_PASSWORD"},{"location":"config/pop3/","text":"If you want to use POP3(S), you have to add the ports 110 and/or 995 (TLS secured) and the environment variable ENABLE_POP3 to your docker-compose.yml : mail : ports : - \"25:25\" - \"143:143\" - \"587:587\" - \"993:993\" - \"110:110\" - \"995:995\" environment : - ENABLE_POP3=1","title":"Mail Delivery with POP3"},{"location":"config/setup.sh/","text":"setup.sh is an administration script that helps with the most common tasks, including initial configuration. It is intented to be used from the host machine, not from within your running container. The latest version of the script is included in the docker-mailserver repository. You may retrieve it at any time by running this command in your console: wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh chmod a+x ./setup.sh Info Make sure to get the setup.sh that comes with the release you're using. Look up the release and the git commit on which this release is based upon by selecting the appropriate tag on GitHub. This can done with the \"Switch branches/tags\" button on GitHub, choosing the right tag. This is done in order to rule out possible inconsistencies between versions. Usage Run ./setup.sh help and you'll get some usage information: SETUP(1) NAME setup.sh - docker-mailserver administration script SYNOPSIS ./setup.sh [ OPTIONS... ] COMMAND [ help | ARGUMENTS... ] COMMAND := { email | alias | quota | config | relay | debug } SUBCOMMAND DESCRIPTION This is the main administration script that you use for all interactions with your mail server. Setup, configuration and much more is done with this script. Please note that the script executes most of the commands inside the container itself. If the image was not found, this script will pull the :latest tag of mailserver/docker-mailserver. This tag refers to the latest release, see the tagging convention in the README under https://github.com/docker-mailserver/docker-mailserver/blob/master/README.md You will be able to see detailed information about the script you are invoking and its arguments by appending help after your command. Currently, this does not work with all scripts. VERSION The current version of this script is backwards compatible with versions of docker-mailserver after 8.0.1. In case that there is not a more recent release, this script is currently only working with the :edge tag. You can download the script for your release by substituting TAG from the following URL, where TAG looks like 'vX.X.X': https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/TAG/setup.sh OPTIONS Config path, container or image adjustments -i IMAGE_NAME Provides the name of the docker-mailserver image. The default value is docker.io/mailserver/docker-mailserver:latest -c CONTAINER_NAME Provides the name of the running container. -p PATH Provides the config folder path. The default is /home/maxwell/Dokumente/github/docker-mailserver/config/ SELinux -z Allows container access to the bind mount content that is shared among multiple containers on a SELinux-enabled host. -Z Allows container access to the bind mount content that is private and unshared with other containers on a SELinux-enabled host. [SUB]COMMANDS COMMAND email := ./setup.sh email add [] ./setup.sh email update [] ./setup.sh email del [ OPTIONS... ] [ ... ] ./setup.sh email restrict [] ./setup.sh email list COMMAND alias := ./setup.sh alias add ./setup.sh alias del ./setup.sh alias list COMMAND quota := ./setup.sh quota set [] ./setup.sh quota del COMMAND config := ./setup.sh config dkim [ ARGUMENTS... ] COMMAND relay := ./setup.sh relay add-domain [] ./setup.sh relay add-auth [] ./setup.sh relay exclude-domain COMMAND debug := ./setup.sh debug fetchmail ./setup.sh debug fail2ban [unban ] ./setup.sh debug show-mail-logs ./setup.sh debug inspect ./setup.sh debug login EXAMPLES ./setup.sh email add test@domain.tld Add the email account test@domain.tld. You will be prompted to input a password afterwards since no password was supplied. ./setup.sh config dkim keysize 2048 domain 'whoami.com,whoareyou.org' Creates keys of length 2048 but in an LDAP setup where domains are not known to Postfix by default, so you need to provide them yourself in a comma-separated list. ./setup.sh config dkim help This will provide you with a detailed explanation on how to use the config dkim command, showing what arguments can be passed and what they do. EXIT STATUS Exit status is 0 if the command was successful. If there was an unexpected error, an error message is shown describing the error. In case of an error, the script will exit with exit status 1.","title":"Your Best Friend setup.sh"},{"location":"config/setup.sh/#usage","text":"Run ./setup.sh help and you'll get some usage information: SETUP(1) NAME setup.sh - docker-mailserver administration script SYNOPSIS ./setup.sh [ OPTIONS... ] COMMAND [ help | ARGUMENTS... ] COMMAND := { email | alias | quota | config | relay | debug } SUBCOMMAND DESCRIPTION This is the main administration script that you use for all interactions with your mail server. Setup, configuration and much more is done with this script. Please note that the script executes most of the commands inside the container itself. If the image was not found, this script will pull the :latest tag of mailserver/docker-mailserver. This tag refers to the latest release, see the tagging convention in the README under https://github.com/docker-mailserver/docker-mailserver/blob/master/README.md You will be able to see detailed information about the script you are invoking and its arguments by appending help after your command. Currently, this does not work with all scripts. VERSION The current version of this script is backwards compatible with versions of docker-mailserver after 8.0.1. In case that there is not a more recent release, this script is currently only working with the :edge tag. You can download the script for your release by substituting TAG from the following URL, where TAG looks like 'vX.X.X': https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/TAG/setup.sh OPTIONS Config path, container or image adjustments -i IMAGE_NAME Provides the name of the docker-mailserver image. The default value is docker.io/mailserver/docker-mailserver:latest -c CONTAINER_NAME Provides the name of the running container. -p PATH Provides the config folder path. The default is /home/maxwell/Dokumente/github/docker-mailserver/config/ SELinux -z Allows container access to the bind mount content that is shared among multiple containers on a SELinux-enabled host. -Z Allows container access to the bind mount content that is private and unshared with other containers on a SELinux-enabled host. [SUB]COMMANDS COMMAND email := ./setup.sh email add [] ./setup.sh email update [] ./setup.sh email del [ OPTIONS... ] [ ... ] ./setup.sh email restrict [] ./setup.sh email list COMMAND alias := ./setup.sh alias add ./setup.sh alias del ./setup.sh alias list COMMAND quota := ./setup.sh quota set [] ./setup.sh quota del COMMAND config := ./setup.sh config dkim [ ARGUMENTS... ] COMMAND relay := ./setup.sh relay add-domain [] ./setup.sh relay add-auth [] ./setup.sh relay exclude-domain COMMAND debug := ./setup.sh debug fetchmail ./setup.sh debug fail2ban [unban ] ./setup.sh debug show-mail-logs ./setup.sh debug inspect ./setup.sh debug login EXAMPLES ./setup.sh email add test@domain.tld Add the email account test@domain.tld. You will be prompted to input a password afterwards since no password was supplied. ./setup.sh config dkim keysize 2048 domain 'whoami.com,whoareyou.org' Creates keys of length 2048 but in an LDAP setup where domains are not known to Postfix by default, so you need to provide them yourself in a comma-separated list. ./setup.sh config dkim help This will provide you with a detailed explanation on how to use the config dkim command, showing what arguments can be passed and what they do. EXIT STATUS Exit status is 0 if the command was successful. If there was an unexpected error, an error message is shown describing the error. In case of an error, the script will exit with exit status 1.","title":"Usage"},{"location":"config/advanced/auth-ldap/","text":"Introduction Getting started with ldap and this mailserver we need to take 3 parts in account: postfix for incoming & outgoing email dovecot for accessing mailboxes saslauthd for SMTP authentication (this can also be delegated to dovecot) Variables to Control Provisioning by the Container Have a look at the ENV page for information on the default values. LDAP_QUERY_FILTER_* Those variables contain the LDAP lookup filters for postfix, using %s as the placeholder for the domain or email address in question. This means that... ...for incoming email, the domain must return an entry for the DOMAIN filter (see virtual_alias_domains ). ...for incoming email, the inboxes which receive the email are chosen by the USER , ALIAS and GROUP filters. The USER filter specifies personal mailboxes, for which only one should exist per address, for example (mail=%s) (also see virtual_mailbox_maps ) The ALIAS filter specifies aliases for mailboxes, using virtual_alias_maps , for example (mailAlias=%s) The GROUP filter specifies the personal mailboxes in a group (for emails that multiple people shall receive), using virtual_alias_maps , for example (mailGroupMember=%s) Technically, there is no difference between ALIAS and GROUP , but ideally you should use ALIAS for personal aliases for a singular person (like ceo@example.org ) and GROUP for multiple people (like hr@example.org ). ...for outgoing email, the sender address is put through the SENDERS filter, and only if the authenticated user is one of the returned entries, the email can be sent. This only applies if SPOOF_PROTECTION=1 . If the SENDERS filter is missing, the USER , ALIAS and GROUP filters will be used in in a disjunction (OR). To for example allow users from the admin group to spoof any sender email address, and to force everyone else to only use their personal mailbox address for outgoing email, you can use something like this: (|(memberOf=cn=admin,*)(mail=%s)) Example A really simple LDAP_QUERY_FILTER configuration, using only the user filter and allowing only admin@* to spoof any sender addresses. - ENABLE_LDAP=1 - LDAP_SERVER_HOST=ldap.example.org - LDAP_SEARCH_BASE=dc=example,dc=org\" - LDAP_BIND_DN=cn=admin,dc=example,dc=org - LDAP_BIND_PW=mypassword - SPOOF_PROTECTION=1 - LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s) - LDAP_QUERY_FILTER_USER=(mail=%s) - LDAP_QUERY_FILTER_ALIAS=(|) # doesn't match anything - LDAP_QUERY_FILTER_GROUP=(|) # doesn't match anything - LDAP_QUERY_FILTER_SENDERS=(|(mail=%s)(mail=admin@*)) DOVECOT_*_FILTER & DOVECOT_*_ATTRS These variables specify the LDAP filters that dovecot uses to determine if a user can log in to their IMAP account, and which mailbox is responsible to receive email for a specific postfix user. This is split into the following two lookups, both using %u as the placeholder for the full login name ( see dovecot documentation for a full list of placeholders ). Usually you only need to set DOVECOT_USER_FILTER , in which case it will be used for both filters. DOVECOT_USER_FILTER is used to get the account details (uid, gid, home directory, quota, ...) of a user. DOVECOT_PASS_FILTER is used to get the password information of the user, and is in pretty much all cases identical to DOVECOT_USER_FILTER (which is the default behaviour if left away). If your directory doesn't have the postfix-book schema installed, then you must change the internal attribute handling for dovecot. For this you have to change the pass_attr and the user_attr mapping, as shown in the example below: - DOVECOT_PASS_ATTRS==user,=password - DOVECOT_USER_ATTRS==home,=mail,=uid,=gid Note For DOVECOT_*_ATTRS , you can replace ldapAttr=dovecotAttr with =dovecotAttr=%{ldap:ldapAttr} for more flexibility, like for example =home=/var/mail/%{ldap:uid} or just =uid=5000 . A list of dovecot attributes can be found in the dovecot documentation . Defaults - DOVECOT_USER_ATTRS=mailHomeDirectory=home,mailUidNumber=uid,mailGidNumber=gid,mailStorageDirectory=mail - DOVECOT_PASS_ATTRS=uniqueIdentifier=user,userPassword=password - DOVECOT_USER_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) Example Setup for a directory that has the qmail-schema installed and uses uid : - DOVECOT_PASS_ATTRS=uid=user,userPassword=password - DOVECOT_USER_ATTRS=homeDirectory=home,qmailUID=uid,qmailGID=gid,mailMessageStore=mail - DOVECOT_USER_FILTER=(&(objectClass=qmailUser)(uid=%u)(accountStatus=active)) The LDAP server configuration for dovecot will be taken mostly from postfix, other options can be found in the environment section in the docs . DOVECOT_AUTH_BIND Set this to yes to enable authentication binds ( more details in the dovecot documentation ). Currently, only DN lookup is supported without further changes to the configuration files, so this is only useful when you want to bind as a readonly user without the permission to read passwords. SASLAUTHD_LDAP_FILTER This filter is used for saslauthd , which is called by postfix when someone is authenticating through SMTP (assuming that SASLAUTHD_MECHANISMS=ldap is being used). Note that you'll need to set up the LDAP server for saslauthd seperately from postfix. The filter variables are explained in detail in the LDAP_SASLAUTHD file , but unfortunately, this method doesn't really support domains right now - that means that %U is the only token that makes sense in this variable. When to use this and how to avoid it Using a separate filter for SMTP authentication allows you to for example allow noreply@example.org to send email, but not log in to IMAP or receive email: (&(mail=%U@example.org)(|(memberOf=cn=email,*)(mail=noreply@example.org))) If you don't want to use a separate filter for SMTP authentication, you can set SASLAUTHD_MECHANISMS=rimap and SASLAUTHD_MECH_OPTIONS=127.0.0.1 to authenticate against dovecot instead - this means that the DOVECOT_USER_FILTER and DOVECOT_PASS_FILTER will be used for SMTP authentication as well. Configure LDAP with saslauthd - ENABLE_SASLAUTHD=1 - SASLAUTHD_MECHANISMS=ldap - SASLAUTHD_LDAP_FILTER=(mail=%U@example.org) Secure Connection with LDAPS or StartTLS To enable LDAPS, all you need to do is to add the protocol to LDAP_SERVER_HOST , for example ldaps://example.org:636 . To enable LDAP over StartTLS (on port 389), you need to set the following environment variables instead (the protocol must not be ldaps:// in this case!): - LDAP_START_TLS=yes - DOVECOT_TLS=yes - SASLAUTHD_LDAP_START_TLS=yes LDAP Setup Examples Basic Setup version : '2' services : mail : image : mailserver/docker-mailserver:latest hostname : mail domainname : example.org container_name : mail ports : - \"25:25\" - \"143:143\" - \"587:587\" - \"993:993\" volumes : - maildata:/var/mail - mailstate:/var/mail-state - ./config/:/tmp/docker-mailserver/ environment : - ENABLE_SPAMASSASSIN=1 - ENABLE_CLAMAV=1 - ENABLE_FAIL2BAN=1 - ENABLE_POSTGREY=1 # >>> Postfix LDAP Integration - ENABLE_LDAP=1 - LDAP_SERVER_HOST=ldap.example.org - LDAP_BIND_DN=cn=admin,ou=users,dc=example,dc=org - LDAP_BIND_PW=mypassword - LDAP_SEARCH_BASE=dc=example,dc=org - LDAP_QUERY_FILTER_DOMAIN=(|(mail=*@%s)(mailAlias=*@%s)(mailGroupMember=*@%s)) - LDAP_QUERY_FILTER_USER=(&(objectClass=inetOrgPerson)(mail=%s)) - LDAP_QUERY_FILTER_ALIAS=(&(objectClass=inetOrgPerson)(mailAlias=%s)) - LDAP_QUERY_FILTER_GROUP=(&(objectClass=inetOrgPerson)(mailGroupMember=%s)) - LDAP_QUERY_FILTER_SENDERS=(&(objectClass=inetOrgPerson)(|(mail=%s)(mailAlias=%s)(mailGroupMember=%s))) - SPOOF_PROTECTION=1 # <<< Postfix LDAP Integration # >>> Dovecot LDAP Integration - DOVECOT_USER_FILTER=(&(objectClass=inetOrgPerson)(mail=%u)) - DOVECOT_PASS_ATTRS=uid=user,userPassword=password - DOVECOT_USER_ATTRS==home=/var/mail/%{ldap:uid},=mail=maildir:~/Maildir,uidNumber=uid,gidNumber=gid # <<< Dovecot LDAP Integration # >>> SASL LDAP Authentication - ENABLE_SASLAUTHD=1 - SASLAUTHD_MECHANISMS=ldap - SASLAUTHD_LDAP_FILTER=(&(mail=%U@example.org)(objectClass=inetOrgPerson)) # <<< SASL LDAP Authentication - ONE_DIR=1 - DMS_DEBUG=0 - SSL_TYPE=letsencrypt - PERMIT_DOCKER=host cap_add : - NET_ADMIN volumes : maildata : driver : local mailstate : driver : local Kopano / Zarafa version : '2' services : mail : image : mailserver/docker-mailserver:latest hostname : mail domainname : domain.com container_name : mail ports : - \"25:25\" - \"143:143\" - \"587:587\" - \"993:993\" volumes : - maildata:/var/mail - mailstate:/var/mail-state - ./config/:/tmp/docker-mailserver/ environment : # We are not using dovecot here - SMTP_ONLY=1 - ENABLE_SPAMASSASSIN=1 - ENABLE_CLAMAV=1 - ENABLE_FAIL2BAN=1 - ENABLE_POSTGREY=1 - SASLAUTHD_PASSWD= # >>> SASL Authentication - ENABLE_SASLAUTHD=1 - SASLAUTHD_LDAP_FILTER=(&(sAMAccountName=%U)(objectClass=person)) - SASLAUTHD_MECHANISMS=ldap # <<< SASL Authentication # >>> Postfix Ldap Integration - ENABLE_LDAP=1 - LDAP_SERVER_HOST= - LDAP_SEARCH_BASE=dc=mydomain,dc=loc - LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=loc - LDAP_BIND_PW=mypassword - LDAP_QUERY_FILTER_USER=(&(objectClass=user)(mail=%s)) - LDAP_QUERY_FILTER_GROUP=(&(objectclass=group)(mail=%s)) - LDAP_QUERY_FILTER_ALIAS=(&(objectClass=user)(otherMailbox=%s)) - LDAP_QUERY_FILTER_DOMAIN=(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE)) # <<< Postfix Ldap Integration # >>> Kopano Integration - ENABLE_POSTFIX_VIRTUAL_TRANSPORT=1 - POSTFIX_DAGENT=lmtp:kopano:2003 # <<< Kopano Integration - ONE_DIR=1 - DMS_DEBUG=0 - SSL_TYPE=letsencrypt - PERMIT_DOCKER=host cap_add : - NET_ADMIN volumes : maildata : driver : local mailstate : driver : local","title":"LDAP Authentication"},{"location":"config/advanced/auth-ldap/#introduction","text":"Getting started with ldap and this mailserver we need to take 3 parts in account: postfix for incoming & outgoing email dovecot for accessing mailboxes saslauthd for SMTP authentication (this can also be delegated to dovecot)","title":"Introduction"},{"location":"config/advanced/auth-ldap/#variables-to-control-provisioning-by-the-container","text":"Have a look at the ENV page for information on the default values.","title":"Variables to Control Provisioning by the Container"},{"location":"config/advanced/auth-ldap/#ldap_query_filter_","text":"Those variables contain the LDAP lookup filters for postfix, using %s as the placeholder for the domain or email address in question. This means that... ...for incoming email, the domain must return an entry for the DOMAIN filter (see virtual_alias_domains ). ...for incoming email, the inboxes which receive the email are chosen by the USER , ALIAS and GROUP filters. The USER filter specifies personal mailboxes, for which only one should exist per address, for example (mail=%s) (also see virtual_mailbox_maps ) The ALIAS filter specifies aliases for mailboxes, using virtual_alias_maps , for example (mailAlias=%s) The GROUP filter specifies the personal mailboxes in a group (for emails that multiple people shall receive), using virtual_alias_maps , for example (mailGroupMember=%s) Technically, there is no difference between ALIAS and GROUP , but ideally you should use ALIAS for personal aliases for a singular person (like ceo@example.org ) and GROUP for multiple people (like hr@example.org ). ...for outgoing email, the sender address is put through the SENDERS filter, and only if the authenticated user is one of the returned entries, the email can be sent. This only applies if SPOOF_PROTECTION=1 . If the SENDERS filter is missing, the USER , ALIAS and GROUP filters will be used in in a disjunction (OR). To for example allow users from the admin group to spoof any sender email address, and to force everyone else to only use their personal mailbox address for outgoing email, you can use something like this: (|(memberOf=cn=admin,*)(mail=%s)) Example A really simple LDAP_QUERY_FILTER configuration, using only the user filter and allowing only admin@* to spoof any sender addresses. - ENABLE_LDAP=1 - LDAP_SERVER_HOST=ldap.example.org - LDAP_SEARCH_BASE=dc=example,dc=org\" - LDAP_BIND_DN=cn=admin,dc=example,dc=org - LDAP_BIND_PW=mypassword - SPOOF_PROTECTION=1 - LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s) - LDAP_QUERY_FILTER_USER=(mail=%s) - LDAP_QUERY_FILTER_ALIAS=(|) # doesn't match anything - LDAP_QUERY_FILTER_GROUP=(|) # doesn't match anything - LDAP_QUERY_FILTER_SENDERS=(|(mail=%s)(mail=admin@*))","title":"LDAP_QUERY_FILTER_*"},{"location":"config/advanced/auth-ldap/#dovecot__filter-dovecot__attrs","text":"These variables specify the LDAP filters that dovecot uses to determine if a user can log in to their IMAP account, and which mailbox is responsible to receive email for a specific postfix user. This is split into the following two lookups, both using %u as the placeholder for the full login name ( see dovecot documentation for a full list of placeholders ). Usually you only need to set DOVECOT_USER_FILTER , in which case it will be used for both filters. DOVECOT_USER_FILTER is used to get the account details (uid, gid, home directory, quota, ...) of a user. DOVECOT_PASS_FILTER is used to get the password information of the user, and is in pretty much all cases identical to DOVECOT_USER_FILTER (which is the default behaviour if left away). If your directory doesn't have the postfix-book schema installed, then you must change the internal attribute handling for dovecot. For this you have to change the pass_attr and the user_attr mapping, as shown in the example below: - DOVECOT_PASS_ATTRS==user,=password - DOVECOT_USER_ATTRS==home,=mail,=uid,=gid Note For DOVECOT_*_ATTRS , you can replace ldapAttr=dovecotAttr with =dovecotAttr=%{ldap:ldapAttr} for more flexibility, like for example =home=/var/mail/%{ldap:uid} or just =uid=5000 . A list of dovecot attributes can be found in the dovecot documentation . Defaults - DOVECOT_USER_ATTRS=mailHomeDirectory=home,mailUidNumber=uid,mailGidNumber=gid,mailStorageDirectory=mail - DOVECOT_PASS_ATTRS=uniqueIdentifier=user,userPassword=password - DOVECOT_USER_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) Example Setup for a directory that has the qmail-schema installed and uses uid : - DOVECOT_PASS_ATTRS=uid=user,userPassword=password - DOVECOT_USER_ATTRS=homeDirectory=home,qmailUID=uid,qmailGID=gid,mailMessageStore=mail - DOVECOT_USER_FILTER=(&(objectClass=qmailUser)(uid=%u)(accountStatus=active)) The LDAP server configuration for dovecot will be taken mostly from postfix, other options can be found in the environment section in the docs .","title":"DOVECOT_*_FILTER & DOVECOT_*_ATTRS"},{"location":"config/advanced/auth-ldap/#dovecot_auth_bind","text":"Set this to yes to enable authentication binds ( more details in the dovecot documentation ). Currently, only DN lookup is supported without further changes to the configuration files, so this is only useful when you want to bind as a readonly user without the permission to read passwords.","title":"DOVECOT_AUTH_BIND"},{"location":"config/advanced/auth-ldap/#saslauthd_ldap_filter","text":"This filter is used for saslauthd , which is called by postfix when someone is authenticating through SMTP (assuming that SASLAUTHD_MECHANISMS=ldap is being used). Note that you'll need to set up the LDAP server for saslauthd seperately from postfix. The filter variables are explained in detail in the LDAP_SASLAUTHD file , but unfortunately, this method doesn't really support domains right now - that means that %U is the only token that makes sense in this variable. When to use this and how to avoid it Using a separate filter for SMTP authentication allows you to for example allow noreply@example.org to send email, but not log in to IMAP or receive email: (&(mail=%U@example.org)(|(memberOf=cn=email,*)(mail=noreply@example.org))) If you don't want to use a separate filter for SMTP authentication, you can set SASLAUTHD_MECHANISMS=rimap and SASLAUTHD_MECH_OPTIONS=127.0.0.1 to authenticate against dovecot instead - this means that the DOVECOT_USER_FILTER and DOVECOT_PASS_FILTER will be used for SMTP authentication as well. Configure LDAP with saslauthd - ENABLE_SASLAUTHD=1 - SASLAUTHD_MECHANISMS=ldap - SASLAUTHD_LDAP_FILTER=(mail=%U@example.org)","title":"SASLAUTHD_LDAP_FILTER"},{"location":"config/advanced/auth-ldap/#secure-connection-with-ldaps-or-starttls","text":"To enable LDAPS, all you need to do is to add the protocol to LDAP_SERVER_HOST , for example ldaps://example.org:636 . To enable LDAP over StartTLS (on port 389), you need to set the following environment variables instead (the protocol must not be ldaps:// in this case!): - LDAP_START_TLS=yes - DOVECOT_TLS=yes - SASLAUTHD_LDAP_START_TLS=yes","title":"Secure Connection with LDAPS or StartTLS"},{"location":"config/advanced/auth-ldap/#ldap-setup-examples","text":"Basic Setup version : '2' services : mail : image : mailserver/docker-mailserver:latest hostname : mail domainname : example.org container_name : mail ports : - \"25:25\" - \"143:143\" - \"587:587\" - \"993:993\" volumes : - maildata:/var/mail - mailstate:/var/mail-state - ./config/:/tmp/docker-mailserver/ environment : - ENABLE_SPAMASSASSIN=1 - ENABLE_CLAMAV=1 - ENABLE_FAIL2BAN=1 - ENABLE_POSTGREY=1 # >>> Postfix LDAP Integration - ENABLE_LDAP=1 - LDAP_SERVER_HOST=ldap.example.org - LDAP_BIND_DN=cn=admin,ou=users,dc=example,dc=org - LDAP_BIND_PW=mypassword - LDAP_SEARCH_BASE=dc=example,dc=org - LDAP_QUERY_FILTER_DOMAIN=(|(mail=*@%s)(mailAlias=*@%s)(mailGroupMember=*@%s)) - LDAP_QUERY_FILTER_USER=(&(objectClass=inetOrgPerson)(mail=%s)) - LDAP_QUERY_FILTER_ALIAS=(&(objectClass=inetOrgPerson)(mailAlias=%s)) - LDAP_QUERY_FILTER_GROUP=(&(objectClass=inetOrgPerson)(mailGroupMember=%s)) - LDAP_QUERY_FILTER_SENDERS=(&(objectClass=inetOrgPerson)(|(mail=%s)(mailAlias=%s)(mailGroupMember=%s))) - SPOOF_PROTECTION=1 # <<< Postfix LDAP Integration # >>> Dovecot LDAP Integration - DOVECOT_USER_FILTER=(&(objectClass=inetOrgPerson)(mail=%u)) - DOVECOT_PASS_ATTRS=uid=user,userPassword=password - DOVECOT_USER_ATTRS==home=/var/mail/%{ldap:uid},=mail=maildir:~/Maildir,uidNumber=uid,gidNumber=gid # <<< Dovecot LDAP Integration # >>> SASL LDAP Authentication - ENABLE_SASLAUTHD=1 - SASLAUTHD_MECHANISMS=ldap - SASLAUTHD_LDAP_FILTER=(&(mail=%U@example.org)(objectClass=inetOrgPerson)) # <<< SASL LDAP Authentication - ONE_DIR=1 - DMS_DEBUG=0 - SSL_TYPE=letsencrypt - PERMIT_DOCKER=host cap_add : - NET_ADMIN volumes : maildata : driver : local mailstate : driver : local Kopano / Zarafa version : '2' services : mail : image : mailserver/docker-mailserver:latest hostname : mail domainname : domain.com container_name : mail ports : - \"25:25\" - \"143:143\" - \"587:587\" - \"993:993\" volumes : - maildata:/var/mail - mailstate:/var/mail-state - ./config/:/tmp/docker-mailserver/ environment : # We are not using dovecot here - SMTP_ONLY=1 - ENABLE_SPAMASSASSIN=1 - ENABLE_CLAMAV=1 - ENABLE_FAIL2BAN=1 - ENABLE_POSTGREY=1 - SASLAUTHD_PASSWD= # >>> SASL Authentication - ENABLE_SASLAUTHD=1 - SASLAUTHD_LDAP_FILTER=(&(sAMAccountName=%U)(objectClass=person)) - SASLAUTHD_MECHANISMS=ldap # <<< SASL Authentication # >>> Postfix Ldap Integration - ENABLE_LDAP=1 - LDAP_SERVER_HOST= - LDAP_SEARCH_BASE=dc=mydomain,dc=loc - LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=loc - LDAP_BIND_PW=mypassword - LDAP_QUERY_FILTER_USER=(&(objectClass=user)(mail=%s)) - LDAP_QUERY_FILTER_GROUP=(&(objectclass=group)(mail=%s)) - LDAP_QUERY_FILTER_ALIAS=(&(objectClass=user)(otherMailbox=%s)) - LDAP_QUERY_FILTER_DOMAIN=(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE)) # <<< Postfix Ldap Integration # >>> Kopano Integration - ENABLE_POSTFIX_VIRTUAL_TRANSPORT=1 - POSTFIX_DAGENT=lmtp:kopano:2003 # <<< Kopano Integration - ONE_DIR=1 - DMS_DEBUG=0 - SSL_TYPE=letsencrypt - PERMIT_DOCKER=host cap_add : - NET_ADMIN volumes : maildata : driver : local mailstate : driver : local","title":"LDAP Setup Examples"},{"location":"config/advanced/full-text-search/","text":"Overview Full-text search allows all messages to be indexed, so that mail clients can quickly and efficiently search messages by their full text content. The dovecot-solr Plugin is used in conjunction with Apache Solr running in a separate container. This is quite straightforward to setup using the following instructions. Setup Steps docker-compose.yml : solr : image : lmmdock/dovecot-solr:latest volumes : - solr-dovecot:/opt/solr/server/solr/dovecot restart : always mailserver : depends_on : - solr image : mailserver/docker-mailserver:latest ... volumes : ... - ./etc/dovecot/conf.d/10-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro ... volumes : solr-dovecot : driver : local etc/dovecot/conf.d/10-plugin.conf : mail_plugins = $mail_plugins fts fts_solr plugin { fts = solr fts_autoindex = yes fts_solr = url=http://solr:8983/solr/dovecot/ } Recreate containers: docker-compose down ; docker-compose up -d Flag all user mailbox FTS indexes as invalid, so they are rescanned on demand when they are next searched: docker-compose exec mailserver doveadm fts rescan -A Further Discussion See #905","title":"Full-Text Search"},{"location":"config/advanced/full-text-search/#overview","text":"Full-text search allows all messages to be indexed, so that mail clients can quickly and efficiently search messages by their full text content. The dovecot-solr Plugin is used in conjunction with Apache Solr running in a separate container. This is quite straightforward to setup using the following instructions.","title":"Overview"},{"location":"config/advanced/full-text-search/#setup-steps","text":"docker-compose.yml : solr : image : lmmdock/dovecot-solr:latest volumes : - solr-dovecot:/opt/solr/server/solr/dovecot restart : always mailserver : depends_on : - solr image : mailserver/docker-mailserver:latest ... volumes : ... - ./etc/dovecot/conf.d/10-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro ... volumes : solr-dovecot : driver : local etc/dovecot/conf.d/10-plugin.conf : mail_plugins = $mail_plugins fts fts_solr plugin { fts = solr fts_autoindex = yes fts_solr = url=http://solr:8983/solr/dovecot/ } Recreate containers: docker-compose down ; docker-compose up -d Flag all user mailbox FTS indexes as invalid, so they are rescanned on demand when they are next searched: docker-compose exec mailserver doveadm fts rescan -A","title":"Setup Steps"},{"location":"config/advanced/full-text-search/#further-discussion","text":"See #905","title":"Further Discussion"},{"location":"config/advanced/ipv6/","text":"Background If your container host supports IPv6, then docker-mailserver will automatically accept IPv6 connections by way of the docker host's IPv6. However, incoming mail will fail SPF checks because they will appear to come from the IPv4 gateway that docker is using to proxy the IPv6 connection ( 172.20.0.1 is the gateway). This can be solved by supporting IPv6 connections all the way to the docker-mailserver container. Setup steps +++ b/serv/docker-compose.yml @@ -1,4 +1,4 @@ -version: '2' +version: '2.1' @@ -32,6 +32,16 @@ services: + ipv6nat: + image: robbertkl/ipv6nat + restart: always + network_mode: \"host\" + cap_add: + - NET_ADMIN + - SYS_MODULE + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /lib/modules:/lib/modules:ro @@ -306,4 +316,13 @@ networks: + default: + driver: bridge + enable_ipv6: true + ipam: + driver: default + config: + - subnet: fd00:0123:4567::/48 + gateway: fd00:0123:4567::1 Further Discussion See #1438","title":"IPv6"},{"location":"config/advanced/ipv6/#background","text":"If your container host supports IPv6, then docker-mailserver will automatically accept IPv6 connections by way of the docker host's IPv6. However, incoming mail will fail SPF checks because they will appear to come from the IPv4 gateway that docker is using to proxy the IPv6 connection ( 172.20.0.1 is the gateway). This can be solved by supporting IPv6 connections all the way to the docker-mailserver container.","title":"Background"},{"location":"config/advanced/ipv6/#setup-steps","text":"+++ b/serv/docker-compose.yml @@ -1,4 +1,4 @@ -version: '2' +version: '2.1' @@ -32,6 +32,16 @@ services: + ipv6nat: + image: robbertkl/ipv6nat + restart: always + network_mode: \"host\" + cap_add: + - NET_ADMIN + - SYS_MODULE + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /lib/modules:/lib/modules:ro @@ -306,4 +316,13 @@ networks: + default: + driver: bridge + enable_ipv6: true + ipam: + driver: default + config: + - subnet: fd00:0123:4567::/48 + gateway: fd00:0123:4567::1","title":"Setup steps"},{"location":"config/advanced/ipv6/#further-discussion","text":"See #1438","title":"Further Discussion"},{"location":"config/advanced/kubernetes/","text":"Deployment Example There is nothing much in deploying mailserver to Kubernetes itself. The things are pretty same as in docker-compose.yml , but with Kubernetes syntax. ConfigMap apiVersion : v1 kind : Namespace metadata : name : mailserver --- kind : ConfigMap apiVersion : v1 metadata : name : mailserver.env.config namespace : mailserver labels : app : mailserver data : OVERRIDE_HOSTNAME : example.com ENABLE_FETCHMAIL : \"0\" FETCHMAIL_POLL : \"120\" ENABLE_SPAMASSASSIN : \"0\" ENABLE_CLAMAV : \"0\" ENABLE_FAIL2BAN : \"0\" ENABLE_POSTGREY : \"0\" ONE_DIR : \"1\" DMS_DEBUG : \"0\" --- kind : ConfigMap apiVersion : v1 metadata : name : mailserver.config namespace : mailserver labels : app : mailserver data : postfix-accounts.cf : | user1@example.com|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1 postfix-virtual.cf : | alias1@example.com user1@dexample.com #dovecot.cf: | # service stats { # unix_listener stats-reader { # group = docker # mode = 0666 # } # unix_listener stats-writer { # group = docker # mode = 0666 # } # } SigningTable : | *@example.com mail._domainkey.example.com KeyTable : | mail._domainkey.example.com example.com:mail:/etc/opendkim/keys/example.com-mail.key TrustedHosts : | 127.0.0.1 localhost #user-patches.sh: | # #!/bin/bash #fetchmail.cf: | Secret apiVersion : v1 kind : Namespace metadata : name : mailserver --- kind : Secret apiVersion : v1 metadata : name : mailserver.opendkim.keys namespace : mailserver labels : app : mailserver type : Opaque data : example.com-mail.key : 'base64-encoded-DKIM-key' Service apiVersion : v1 kind : Namespace metadata : name : mailserver --- kind : Service apiVersion : v1 metadata : name : mailserver namespace : mailserver labels : app : mailserver spec : selector : app : mailserver ports : - name : smtp port : 25 targetPort : smtp - name : smtp-secure port : 465 targetPort : smtp-secure - name : smtp-auth port : 587 targetPort : smtp-auth - name : imap port : 143 targetPort : imap - name : imap-secure port : 993 targetPort : imap-secure Deployment apiVersion : v1 kind : Namespace metadata : name : mailserver --- apiVersion : apps/v1 kind : Deployment metadata : name : mailserver namespace : mailserver spec : replicas : 1 selector : matchLabels : app : mailserver template : metadata : labels : app : mailserver role : mail tier : backend spec : #nodeSelector: # kubernetes.io/hostname: local.k8s #initContainers: #- name: init-myservice # image: busybox # command: [\"/bin/sh\", \"-c\", \"cp /tmp/user-patches.sh /tmp/files\"] # volumeMounts: # - name: config # subPath: user-patches.sh # mountPath: /tmp/user-patches.sh # readOnly: true # - name: tmp-files # mountPath: /tmp/files containers : - name : docker-mailserver image : mailserver/docker-mailserver:latest imagePullPolicy : Always securityContext : capabilities : # If Fail2Ban is not enabled, you can remove NET_ADMIN. # If you are running on CRI-O, you will need the SYS_CHROOT capability, # as it is no longer a default capability. add : [ \"NET_ADMIN\" , \"SYS_PTRACE\" , \"SYS_CHROOT\" ] volumeMounts : - name : config subPath : postfix-accounts.cf mountPath : /tmp/docker-mailserver/postfix-accounts.cf readOnly : true #- name: config # subPath: postfix-main.cf # mountPath: /tmp/docker-mailserver/postfix-main.cf # readOnly: true - name : config subPath : postfix-virtual.cf mountPath : /tmp/docker-mailserver/postfix-virtual.cf readOnly : true - name : config subPath : fetchmail.cf mountPath : /tmp/docker-mailserver/fetchmail.cf readOnly : true - name : config subPath : dovecot.cf mountPath : /tmp/docker-mailserver/dovecot.cf readOnly : true #- name: config # subPath: user1.example.com.dovecot.sieve # mountPath: /tmp/docker-mailserver/user1@example.com.dovecot.sieve # readOnly: true #- name: tmp-files # subPath: user-patches.sh # mountPath: /tmp/docker-mailserver/user-patches.sh - name : config subPath : SigningTable mountPath : /tmp/docker-mailserver/opendkim/SigningTable readOnly : true - name : config subPath : KeyTable mountPath : /tmp/docker-mailserver/opendkim/KeyTable readOnly : true - name : config subPath : TrustedHosts mountPath : /tmp/docker-mailserver/opendkim/TrustedHosts readOnly : true - name : opendkim-keys mountPath : /tmp/docker-mailserver/opendkim/keys readOnly : true - name : data mountPath : /var/mail subPath : data - name : data mountPath : /var/mail-state subPath : state - name : data mountPath : /var/log/mail subPath : log ports : - name : smtp containerPort : 25 protocol : TCP - name : smtp-secure containerPort : 465 protocol : TCP - name : smtp-auth containerPort : 587 - name : imap containerPort : 143 protocol : TCP - name : imap-secure containerPort : 993 protocol : TCP envFrom : - configMapRef : name : mailserver.env.config volumes : - name : config configMap : name : mailserver.config - name : opendkim-keys secret : secretName : mailserver.opendkim.keys - name : data persistentVolumeClaim : claimName : mail-storage - name : tmp-files emptyDir : {} Warning Any sensitive data (keys, etc) should be deployed via Secrets . Other configuration just fits well into ConfigMaps . Note Make sure that Pod is assigned to specific Node in case you're using volume for data directly with hostPath . Otherwise Pod can be rescheduled on a different Node and previous data won't be found. Except the case when you're using some shared filesystem on your Nodes. Note If you experience issues with processes crashing showing an error like operation not permitted or postfix/pickup[987]: fatal: chroot(/var/spool/postfix): Operation not permitted , then you should add the SYS_CHROOT capability. Runtimes like CRI-O do not ship with this capability by default. Exposing to the Outside World The hard part with Kubernetes is to expose deployed mailserver to outside world. Kubernetes provides multiple ways for doing that. Each has its downsides and complexity. The major problem with exposing mailserver to outside world in Kubernetes is to preserve real client IP . Real client IP is required by mailserver for performing IP-based SPF checks and spam checks. Preserving real client IP is relatively non-trivial in Kubernetes and most exposing ways do not provide it. So, it's up to you to decide which exposing way suits better your needs in a price of complexity. If you do not require SPF checks for incoming mails you may disable them in Postfix configuration by dropping following line (which removes check_policy_service unix:private/policyd-spf option): Example kind : ConfigMap apiVersion : v1 metadata : name : mailserver.config labels : app : mailserver data : postfix-main.cf : | smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net # ... --- kind : Deployment apiVersion : extensions/v1beta1 metadata : name : mailserver # ... volumeMounts : - name : config subPath : postfix-main.cf mountPath : /tmp/docker-mailserver/postfix-main.cf readOnly : true External IPs Service The simplest way is to expose mailserver as a Service with external IPs . Example kind : Service apiVersion : v1 metadata : name : mailserver labels : app : mailserver spec : selector : app : mailserver ports : - name : smtp port : 25 targetPort : smtp # ... externalIPs : - 80.11.12.10 Downsides Real client IP is not preserved , so SPF check of incoming mail will fail. Requirement to specify exposed IPs explicitly. Proxy port to Service The Proxy Pod helps to avoid necessity of specifying external IPs explicitly. This comes in price of complexity: you must deploy Proxy Pod on each Node you want to expose mailserver on. Downsides Real client IP is not preserved , so SPF check of incoming mail will fail. Bind to concrete Node and use host network The simplest way to preserve real client IP is to use hostPort and hostNetwork: true in the mailserver Pod . This comes in price of availability: you can talk to mailserver from outside world only via IPs of Node where mailserver is deployed. Example kind : Deployment apiVersion : extensions/v1beta1 metadata : name : mailserver # ... spec : hostNetwork : true # ... containers : # ... ports : - name : smtp containerPort : 25 hostPort : 25 - name : smtp-auth containerPort : 587 hostPort : 587 - name : imap-secure containerPort : 993 hostPort : 993 # ... Downsides Not possible to access mailserver via other cluster Nodes, only via the one mailserver deployed at. Every Port within the Container is exposed on the Host side, regardless of what the ports section in the Configuration defines. Proxy Port to Service via PROXY Protocol This way is ideologically the same as using Proxy Pod , but instead of a separate proxy pod, you configure your ingress to proxy TCP traffic to the mailserver pod using the PROXY protocol, which preserves the real client IP. Configure your Ingress With an NGINX ingress controller , set externalTrafficPolicy: Local for its service, and add the following to the TCP services config map (as described here ): 25 : \"mailserver/mailserver:25::PROXY\" 465 : \"mailserver/mailserver:465::PROXY\" 587 : \"mailserver/mailserver:587::PROXY\" 993 : \"mailserver/mailserver:993::PROXY\" With HAProxy , the configuration should look similar to the above. If you know what it actually looks like, add an example here. Configure the Mailserver Then, configure both Postfix and Dovecot to expect the PROXY protocol: Example kind : ConfigMap apiVersion : v1 metadata : name : mailserver.config labels : app : mailserver data : postfix-main.cf : | postscreen_upstream_proxy_protocol = haproxy postfix-master.cf : | smtp/inet/postscreen_upstream_proxy_protocol=haproxy submission/inet/smtpd_upstream_proxy_protocol=haproxy smtps/inet/smtpd_upstream_proxy_protocol=haproxy dovecot.cf : | # Assuming your ingress controller is bound to 10.0.0.0/8 haproxy_trusted_networks = 10.0.0.0/8, 127.0.0.0/8 service imap-login { inet_listener imap { haproxy = yes } inet_listener imaps { haproxy = yes } } # ... --- kind : Deployment apiVersion : extensions/v1beta1 metadata : name : mailserver spec : template : spec : containers : - name : docker-mailserver volumeMounts : - name : config subPath : postfix-main.cf mountPath : /tmp/docker-mailserver/postfix-main.cf readOnly : true - name : config subPath : postfix-master.cf mountPath : /tmp/docker-mailserver/postfix-master.cf readOnly : true - name : config subPath : dovecot.cf mountPath : /tmp/docker-mailserver/dovecot.cf readOnly : true Downsides Not possible to access mailserver via inner cluster Kubernetes DNS, as PROXY protocol is required for incoming connections. Let's Encrypt Certificates Kube-Lego may be used for a role of Let's Encrypt client. It works with Kubernetes Ingress Resources and automatically issues/manages certificates/keys for exposed services via Ingresses. Example kind : Ingress apiVersion : extensions/v1beta1 metadata : name : mailserver labels : app : mailserver annotations : kubernetes.io/tls-acme : 'true' spec : rules : - host : example.com http : paths : - path : / backend : serviceName : default-backend servicePort : 80 tls : - secretName : mailserver.tls hosts : - example.com Now, you can use Let's Encrypt cert and key from mailserver.tls Secret in your Pod spec: Example # ... env : - name : SSL_TYPE value : 'manual' - name : SSL_CERT_PATH value : '/etc/ssl/mailserver/tls.crt' - name : SSL_KEY_PATH value : '/etc/ssl/mailserver/tls.key' # ... volumeMounts : - name : tls mountPath : /etc/ssl/mailserver readOnly : true # ... volumes : - name : tls secret : secretName : mailserver.tls","title":"Kubernetes"},{"location":"config/advanced/kubernetes/#deployment-example","text":"There is nothing much in deploying mailserver to Kubernetes itself. The things are pretty same as in docker-compose.yml , but with Kubernetes syntax. ConfigMap apiVersion : v1 kind : Namespace metadata : name : mailserver --- kind : ConfigMap apiVersion : v1 metadata : name : mailserver.env.config namespace : mailserver labels : app : mailserver data : OVERRIDE_HOSTNAME : example.com ENABLE_FETCHMAIL : \"0\" FETCHMAIL_POLL : \"120\" ENABLE_SPAMASSASSIN : \"0\" ENABLE_CLAMAV : \"0\" ENABLE_FAIL2BAN : \"0\" ENABLE_POSTGREY : \"0\" ONE_DIR : \"1\" DMS_DEBUG : \"0\" --- kind : ConfigMap apiVersion : v1 metadata : name : mailserver.config namespace : mailserver labels : app : mailserver data : postfix-accounts.cf : | user1@example.com|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1 postfix-virtual.cf : | alias1@example.com user1@dexample.com #dovecot.cf: | # service stats { # unix_listener stats-reader { # group = docker # mode = 0666 # } # unix_listener stats-writer { # group = docker # mode = 0666 # } # } SigningTable : | *@example.com mail._domainkey.example.com KeyTable : | mail._domainkey.example.com example.com:mail:/etc/opendkim/keys/example.com-mail.key TrustedHosts : | 127.0.0.1 localhost #user-patches.sh: | # #!/bin/bash #fetchmail.cf: | Secret apiVersion : v1 kind : Namespace metadata : name : mailserver --- kind : Secret apiVersion : v1 metadata : name : mailserver.opendkim.keys namespace : mailserver labels : app : mailserver type : Opaque data : example.com-mail.key : 'base64-encoded-DKIM-key' Service apiVersion : v1 kind : Namespace metadata : name : mailserver --- kind : Service apiVersion : v1 metadata : name : mailserver namespace : mailserver labels : app : mailserver spec : selector : app : mailserver ports : - name : smtp port : 25 targetPort : smtp - name : smtp-secure port : 465 targetPort : smtp-secure - name : smtp-auth port : 587 targetPort : smtp-auth - name : imap port : 143 targetPort : imap - name : imap-secure port : 993 targetPort : imap-secure Deployment apiVersion : v1 kind : Namespace metadata : name : mailserver --- apiVersion : apps/v1 kind : Deployment metadata : name : mailserver namespace : mailserver spec : replicas : 1 selector : matchLabels : app : mailserver template : metadata : labels : app : mailserver role : mail tier : backend spec : #nodeSelector: # kubernetes.io/hostname: local.k8s #initContainers: #- name: init-myservice # image: busybox # command: [\"/bin/sh\", \"-c\", \"cp /tmp/user-patches.sh /tmp/files\"] # volumeMounts: # - name: config # subPath: user-patches.sh # mountPath: /tmp/user-patches.sh # readOnly: true # - name: tmp-files # mountPath: /tmp/files containers : - name : docker-mailserver image : mailserver/docker-mailserver:latest imagePullPolicy : Always securityContext : capabilities : # If Fail2Ban is not enabled, you can remove NET_ADMIN. # If you are running on CRI-O, you will need the SYS_CHROOT capability, # as it is no longer a default capability. add : [ \"NET_ADMIN\" , \"SYS_PTRACE\" , \"SYS_CHROOT\" ] volumeMounts : - name : config subPath : postfix-accounts.cf mountPath : /tmp/docker-mailserver/postfix-accounts.cf readOnly : true #- name: config # subPath: postfix-main.cf # mountPath: /tmp/docker-mailserver/postfix-main.cf # readOnly: true - name : config subPath : postfix-virtual.cf mountPath : /tmp/docker-mailserver/postfix-virtual.cf readOnly : true - name : config subPath : fetchmail.cf mountPath : /tmp/docker-mailserver/fetchmail.cf readOnly : true - name : config subPath : dovecot.cf mountPath : /tmp/docker-mailserver/dovecot.cf readOnly : true #- name: config # subPath: user1.example.com.dovecot.sieve # mountPath: /tmp/docker-mailserver/user1@example.com.dovecot.sieve # readOnly: true #- name: tmp-files # subPath: user-patches.sh # mountPath: /tmp/docker-mailserver/user-patches.sh - name : config subPath : SigningTable mountPath : /tmp/docker-mailserver/opendkim/SigningTable readOnly : true - name : config subPath : KeyTable mountPath : /tmp/docker-mailserver/opendkim/KeyTable readOnly : true - name : config subPath : TrustedHosts mountPath : /tmp/docker-mailserver/opendkim/TrustedHosts readOnly : true - name : opendkim-keys mountPath : /tmp/docker-mailserver/opendkim/keys readOnly : true - name : data mountPath : /var/mail subPath : data - name : data mountPath : /var/mail-state subPath : state - name : data mountPath : /var/log/mail subPath : log ports : - name : smtp containerPort : 25 protocol : TCP - name : smtp-secure containerPort : 465 protocol : TCP - name : smtp-auth containerPort : 587 - name : imap containerPort : 143 protocol : TCP - name : imap-secure containerPort : 993 protocol : TCP envFrom : - configMapRef : name : mailserver.env.config volumes : - name : config configMap : name : mailserver.config - name : opendkim-keys secret : secretName : mailserver.opendkim.keys - name : data persistentVolumeClaim : claimName : mail-storage - name : tmp-files emptyDir : {} Warning Any sensitive data (keys, etc) should be deployed via Secrets . Other configuration just fits well into ConfigMaps . Note Make sure that Pod is assigned to specific Node in case you're using volume for data directly with hostPath . Otherwise Pod can be rescheduled on a different Node and previous data won't be found. Except the case when you're using some shared filesystem on your Nodes. Note If you experience issues with processes crashing showing an error like operation not permitted or postfix/pickup[987]: fatal: chroot(/var/spool/postfix): Operation not permitted , then you should add the SYS_CHROOT capability. Runtimes like CRI-O do not ship with this capability by default.","title":"Deployment Example"},{"location":"config/advanced/kubernetes/#exposing-to-the-outside-world","text":"The hard part with Kubernetes is to expose deployed mailserver to outside world. Kubernetes provides multiple ways for doing that. Each has its downsides and complexity. The major problem with exposing mailserver to outside world in Kubernetes is to preserve real client IP . Real client IP is required by mailserver for performing IP-based SPF checks and spam checks. Preserving real client IP is relatively non-trivial in Kubernetes and most exposing ways do not provide it. So, it's up to you to decide which exposing way suits better your needs in a price of complexity. If you do not require SPF checks for incoming mails you may disable them in Postfix configuration by dropping following line (which removes check_policy_service unix:private/policyd-spf option): Example kind : ConfigMap apiVersion : v1 metadata : name : mailserver.config labels : app : mailserver data : postfix-main.cf : | smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net # ... --- kind : Deployment apiVersion : extensions/v1beta1 metadata : name : mailserver # ... volumeMounts : - name : config subPath : postfix-main.cf mountPath : /tmp/docker-mailserver/postfix-main.cf readOnly : true","title":"Exposing to the Outside World"},{"location":"config/advanced/kubernetes/#external-ips-service","text":"The simplest way is to expose mailserver as a Service with external IPs . Example kind : Service apiVersion : v1 metadata : name : mailserver labels : app : mailserver spec : selector : app : mailserver ports : - name : smtp port : 25 targetPort : smtp # ... externalIPs : - 80.11.12.10 Downsides Real client IP is not preserved , so SPF check of incoming mail will fail. Requirement to specify exposed IPs explicitly.","title":"External IPs Service"},{"location":"config/advanced/kubernetes/#proxy-port-to-service","text":"The Proxy Pod helps to avoid necessity of specifying external IPs explicitly. This comes in price of complexity: you must deploy Proxy Pod on each Node you want to expose mailserver on. Downsides Real client IP is not preserved , so SPF check of incoming mail will fail.","title":"Proxy port to Service"},{"location":"config/advanced/kubernetes/#bind-to-concrete-node-and-use-host-network","text":"The simplest way to preserve real client IP is to use hostPort and hostNetwork: true in the mailserver Pod . This comes in price of availability: you can talk to mailserver from outside world only via IPs of Node where mailserver is deployed. Example kind : Deployment apiVersion : extensions/v1beta1 metadata : name : mailserver # ... spec : hostNetwork : true # ... containers : # ... ports : - name : smtp containerPort : 25 hostPort : 25 - name : smtp-auth containerPort : 587 hostPort : 587 - name : imap-secure containerPort : 993 hostPort : 993 # ... Downsides Not possible to access mailserver via other cluster Nodes, only via the one mailserver deployed at. Every Port within the Container is exposed on the Host side, regardless of what the ports section in the Configuration defines.","title":"Bind to concrete Node and use host network"},{"location":"config/advanced/kubernetes/#proxy-port-to-service-via-proxy-protocol","text":"This way is ideologically the same as using Proxy Pod , but instead of a separate proxy pod, you configure your ingress to proxy TCP traffic to the mailserver pod using the PROXY protocol, which preserves the real client IP.","title":"Proxy Port to Service via PROXY Protocol"},{"location":"config/advanced/kubernetes/#configure-your-ingress","text":"With an NGINX ingress controller , set externalTrafficPolicy: Local for its service, and add the following to the TCP services config map (as described here ): 25 : \"mailserver/mailserver:25::PROXY\" 465 : \"mailserver/mailserver:465::PROXY\" 587 : \"mailserver/mailserver:587::PROXY\" 993 : \"mailserver/mailserver:993::PROXY\" With HAProxy , the configuration should look similar to the above. If you know what it actually looks like, add an example here.","title":"Configure your Ingress"},{"location":"config/advanced/kubernetes/#configure-the-mailserver","text":"Then, configure both Postfix and Dovecot to expect the PROXY protocol: Example kind : ConfigMap apiVersion : v1 metadata : name : mailserver.config labels : app : mailserver data : postfix-main.cf : | postscreen_upstream_proxy_protocol = haproxy postfix-master.cf : | smtp/inet/postscreen_upstream_proxy_protocol=haproxy submission/inet/smtpd_upstream_proxy_protocol=haproxy smtps/inet/smtpd_upstream_proxy_protocol=haproxy dovecot.cf : | # Assuming your ingress controller is bound to 10.0.0.0/8 haproxy_trusted_networks = 10.0.0.0/8, 127.0.0.0/8 service imap-login { inet_listener imap { haproxy = yes } inet_listener imaps { haproxy = yes } } # ... --- kind : Deployment apiVersion : extensions/v1beta1 metadata : name : mailserver spec : template : spec : containers : - name : docker-mailserver volumeMounts : - name : config subPath : postfix-main.cf mountPath : /tmp/docker-mailserver/postfix-main.cf readOnly : true - name : config subPath : postfix-master.cf mountPath : /tmp/docker-mailserver/postfix-master.cf readOnly : true - name : config subPath : dovecot.cf mountPath : /tmp/docker-mailserver/dovecot.cf readOnly : true Downsides Not possible to access mailserver via inner cluster Kubernetes DNS, as PROXY protocol is required for incoming connections.","title":"Configure the Mailserver"},{"location":"config/advanced/kubernetes/#lets-encrypt-certificates","text":"Kube-Lego may be used for a role of Let's Encrypt client. It works with Kubernetes Ingress Resources and automatically issues/manages certificates/keys for exposed services via Ingresses. Example kind : Ingress apiVersion : extensions/v1beta1 metadata : name : mailserver labels : app : mailserver annotations : kubernetes.io/tls-acme : 'true' spec : rules : - host : example.com http : paths : - path : / backend : serviceName : default-backend servicePort : 80 tls : - secretName : mailserver.tls hosts : - example.com Now, you can use Let's Encrypt cert and key from mailserver.tls Secret in your Pod spec: Example # ... env : - name : SSL_TYPE value : 'manual' - name : SSL_CERT_PATH value : '/etc/ssl/mailserver/tls.crt' - name : SSL_KEY_PATH value : '/etc/ssl/mailserver/tls.key' # ... volumeMounts : - name : tls mountPath : /etc/ssl/mailserver readOnly : true # ... volumes : - name : tls secret : secretName : mailserver.tls","title":"Let's Encrypt Certificates"},{"location":"config/advanced/mail-fetchmail/","text":"To enable the fetchmail service to retrieve e-mails set the environment variable ENABLE_FETCHMAIL to 1 . Your docker-compose.yml file should look like following snippet: environment : - ENABLE_FETCHMAIL=1 - FETCHMAIL_POLL=300 Generate a file called fetchmail.cf and place it in the config folder. Your docker-mailserver folder should look like this example: \u251c\u2500\u2500 config \u2502 \u251c\u2500\u2500 dovecot.cf \u2502 \u251c\u2500\u2500 fetchmail.cf \u2502 \u251c\u2500\u2500 postfix-accounts.cf \u2502 \u2514\u2500\u2500 postfix-virtual.cf \u251c\u2500\u2500 docker-compose.yml \u2514\u2500\u2500 README.md Configuration A detailed description of the configuration options can be found in the online version of the manual page . IMAP Configuration Example poll 'imap.example.com' proto imap user 'username' pass 'secret' is 'user1@domain.tld' ssl POP3 Configuration Example poll 'pop3.example.com' proto pop3 user 'username' pass 'secret' is 'user2@domain.tld' ssl Caution Don\u2019t forget the last line: eg: is 'user1@domain.tld' . After is you have to specify one email address from the configuration file config/postfix-accounts.cf . More details how to configure fetchmail can be found in the fetchmail man page in the chapter \u201cThe run control file\u201d . Polling Interval By default the fetchmail service searches every 5 minutes for new mails on your external mail accounts. You can override this default value by changing the ENV variable FETCHMAIL_POLL : environment : - FETCHMAIL_POLL=60 You must specify a numeric argument which is a polling interval in seconds. The example above polls every minute for new mails. Debugging To debug your fetchmail.cf configuration run this command: ./setup.sh debug fetchmail For more informations about the configuration script setup.sh read the corresponding docs . Here a sample output of ./setup.sh debug fetchmail : fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:09 2016: poll started Trying to connect to 132.245.48.18/995...connected. fetchmail: Server certificate: fetchmail: Issuer Organization: Microsoft Corporation fetchmail: Issuer CommonName: Microsoft IT SSL SHA2 fetchmail: Subject CommonName: outlook.com fetchmail: Subject Alternative Name: outlook.com fetchmail: Subject Alternative Name: *.outlook.com fetchmail: Subject Alternative Name: office365.com fetchmail: Subject Alternative Name: *.office365.com fetchmail: Subject Alternative Name: *.live.com fetchmail: Subject Alternative Name: *.internal.outlook.com fetchmail: Subject Alternative Name: *.outlook.office365.com fetchmail: Subject Alternative Name: outlook.office.com fetchmail: Subject Alternative Name: attachment.outlook.office.net fetchmail: Subject Alternative Name: attachment.outlook.officeppe.net fetchmail: Subject Alternative Name: *.office.com fetchmail: outlook.office365.com key fingerprint: 3A:A4:58:42:56:CD:BD:11:19:5B:CF:1E:85:16:8E:4D fetchmail: POP3< +OK The Microsoft Exchange POP3 service is ready. [SABFADEAUABSADAAMQBDAEEAMAAwADAANwAuAGUAdQByAHAAcgBkADAAMQAuAHAAcgBvAGQALgBlAHgAYwBoAGEAbgBnAGUAbABhAGIAcwAuAGMAbwBtAA==] fetchmail: POP3> CAPA fetchmail: POP3< +OK fetchmail: POP3< TOP fetchmail: POP3< UIDL fetchmail: POP3< SASL PLAIN fetchmail: POP3< USER fetchmail: POP3< . fetchmail: POP3> USER user1@outlook.com fetchmail: POP3< +OK fetchmail: POP3> PASS * fetchmail: POP3< +OK User successfully logged on. fetchmail: POP3> STAT fetchmail: POP3< +OK 0 0 fetchmail: No mail for user1@outlook.com at outlook.office365.com fetchmail: POP3> QUIT fetchmail: POP3< +OK Microsoft Exchange Server 2016 POP3 server signing off. fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:11 2016: poll completed fetchmail: normal termination, status 1","title":"Email Gathering with Fetchmail"},{"location":"config/advanced/mail-fetchmail/#configuration","text":"A detailed description of the configuration options can be found in the online version of the manual page .","title":"Configuration"},{"location":"config/advanced/mail-fetchmail/#imap-configuration","text":"Example poll 'imap.example.com' proto imap user 'username' pass 'secret' is 'user1@domain.tld' ssl","title":"IMAP Configuration"},{"location":"config/advanced/mail-fetchmail/#pop3-configuration","text":"Example poll 'pop3.example.com' proto pop3 user 'username' pass 'secret' is 'user2@domain.tld' ssl Caution Don\u2019t forget the last line: eg: is 'user1@domain.tld' . After is you have to specify one email address from the configuration file config/postfix-accounts.cf . More details how to configure fetchmail can be found in the fetchmail man page in the chapter \u201cThe run control file\u201d .","title":"POP3 Configuration"},{"location":"config/advanced/mail-fetchmail/#polling-interval","text":"By default the fetchmail service searches every 5 minutes for new mails on your external mail accounts. You can override this default value by changing the ENV variable FETCHMAIL_POLL : environment : - FETCHMAIL_POLL=60 You must specify a numeric argument which is a polling interval in seconds. The example above polls every minute for new mails.","title":"Polling Interval"},{"location":"config/advanced/mail-fetchmail/#debugging","text":"To debug your fetchmail.cf configuration run this command: ./setup.sh debug fetchmail For more informations about the configuration script setup.sh read the corresponding docs . Here a sample output of ./setup.sh debug fetchmail : fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:09 2016: poll started Trying to connect to 132.245.48.18/995...connected. fetchmail: Server certificate: fetchmail: Issuer Organization: Microsoft Corporation fetchmail: Issuer CommonName: Microsoft IT SSL SHA2 fetchmail: Subject CommonName: outlook.com fetchmail: Subject Alternative Name: outlook.com fetchmail: Subject Alternative Name: *.outlook.com fetchmail: Subject Alternative Name: office365.com fetchmail: Subject Alternative Name: *.office365.com fetchmail: Subject Alternative Name: *.live.com fetchmail: Subject Alternative Name: *.internal.outlook.com fetchmail: Subject Alternative Name: *.outlook.office365.com fetchmail: Subject Alternative Name: outlook.office.com fetchmail: Subject Alternative Name: attachment.outlook.office.net fetchmail: Subject Alternative Name: attachment.outlook.officeppe.net fetchmail: Subject Alternative Name: *.office.com fetchmail: outlook.office365.com key fingerprint: 3A:A4:58:42:56:CD:BD:11:19:5B:CF:1E:85:16:8E:4D fetchmail: POP3< +OK The Microsoft Exchange POP3 service is ready. [SABFADEAUABSADAAMQBDAEEAMAAwADAANwAuAGUAdQByAHAAcgBkADAAMQAuAHAAcgBvAGQALgBlAHgAYwBoAGEAbgBnAGUAbABhAGIAcwAuAGMAbwBtAA==] fetchmail: POP3> CAPA fetchmail: POP3< +OK fetchmail: POP3< TOP fetchmail: POP3< UIDL fetchmail: POP3< SASL PLAIN fetchmail: POP3< USER fetchmail: POP3< . fetchmail: POP3> USER user1@outlook.com fetchmail: POP3< +OK fetchmail: POP3> PASS * fetchmail: POP3< +OK User successfully logged on. fetchmail: POP3> STAT fetchmail: POP3< +OK 0 0 fetchmail: No mail for user1@outlook.com at outlook.office365.com fetchmail: POP3> QUIT fetchmail: POP3< +OK Microsoft Exchange Server 2016 POP3 server signing off. fetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:11 2016: poll completed fetchmail: normal termination, status 1","title":"Debugging"},{"location":"config/advanced/mail-sieve/","text":"User-Defined Sieve Filters Sieve allows to specify filtering rules for incoming emails that allow for example sorting mails into different folders depending on the title of an email. There are global and user specific filters which are filtering the incoming emails in the following order: Global-before -> User specific -> Global-after Global filters are applied to EVERY incoming mail for EVERY email address. To specify a global Sieve filter provide a config/before.dovecot.sieve or a config/after.dovecot.sieve file with your filter rules. If any filter in this filtering chain discards an incoming mail, the delivery process will stop as well and the mail will not reach any following filters(e.g. global-before stops an incoming spam mail: The mail will get discarded and a user-specific filter won't get applied.) To specify a user-defined Sieve filter place a .dovecot.sieve file into a virtual user's mail folder e.g. /var/mail/domain.com/user1/.dovecot.sieve . If this file exists dovecot will apply the filtering rules. It's even possible to install a user provided Sieve filter at startup during users setup: simply include a Sieve file in the config path for each user login that need a filter. The file name provided should be in the form .dovecot.sieve , so for example for user1@domain.tld you should provide a Sieve file named config/user1@domain.tld.dovecot.sieve . An example of a sieve filter that moves mails to a folder INBOX/spam depending on the sender address: Example require [ \"fileinto\" , \"reject\" ]; if address :contains [ \"From\" ] \"spam@spam.com\" { fileinto \"INBOX.spam\" ; } else { keep ; } Warning That folders have to exist beforehand if sieve should move them. Another example of a sieve filter that forward mails to a different address: Example require [ \"copy\" ]; redirect :copy \"user2@otherdomain.tld\" ; Just forward all incoming emails and do not save them locally: Example redirect \"user2@otherdomain.tld\" ; You can also use external programs to filter or pipe (process) messages by adding executable scripts in config/sieve-pipe or config/sieve-filter . This can be used in lieu of a local alias file, for instance to forward an email to a webservice. These programs can then be referenced by filename, by all users. Note that the process running the scripts run as a privileged user. For further information see Dovecot's wiki . require [ \"vnd.dovecot.pipe\" ]; pipe \"external-program\" ; For more examples or a detailed description of the Sieve language have a look at the official site . Other resources are available on the internet where you can find several examples . Manage Sieve The Manage Sieve extension allows users to modify their Sieve script by themselves. The authentication mechanisms are the same as for the main dovecot service. ManageSieve runs on port 4190 and needs to be enabled using the ENABLE_MANAGESIEVE=1 environment variable. Example # docker-compose.yml ports : - \"4190:4190\" environment : - ENABLE_MANAGESIEVE=1 All user defined sieve scripts that are managed by ManageSieve are stored in the user's home folder in /var/mail/domain.com/user1/sieve . Just one sieve script might be active for a user and is sym-linked to /var/mail/domain.com/user1/.dovecot.sieve automatically. Note ManageSieve makes sure to not overwrite an existing .dovecot.sieve file. If a user activates a new sieve script the old one is backuped and moved to the sieve folder. The extension is known to work with the following ManageSieve clients: Sieve Editor a portable standalone application based on the former Thunderbird plugin. Kmail the mail client of KDE 's Kontact Suite.","title":"Email Filtering with Sieve"},{"location":"config/advanced/mail-sieve/#user-defined-sieve-filters","text":"Sieve allows to specify filtering rules for incoming emails that allow for example sorting mails into different folders depending on the title of an email. There are global and user specific filters which are filtering the incoming emails in the following order: Global-before -> User specific -> Global-after Global filters are applied to EVERY incoming mail for EVERY email address. To specify a global Sieve filter provide a config/before.dovecot.sieve or a config/after.dovecot.sieve file with your filter rules. If any filter in this filtering chain discards an incoming mail, the delivery process will stop as well and the mail will not reach any following filters(e.g. global-before stops an incoming spam mail: The mail will get discarded and a user-specific filter won't get applied.) To specify a user-defined Sieve filter place a .dovecot.sieve file into a virtual user's mail folder e.g. /var/mail/domain.com/user1/.dovecot.sieve . If this file exists dovecot will apply the filtering rules. It's even possible to install a user provided Sieve filter at startup during users setup: simply include a Sieve file in the config path for each user login that need a filter. The file name provided should be in the form .dovecot.sieve , so for example for user1@domain.tld you should provide a Sieve file named config/user1@domain.tld.dovecot.sieve . An example of a sieve filter that moves mails to a folder INBOX/spam depending on the sender address: Example require [ \"fileinto\" , \"reject\" ]; if address :contains [ \"From\" ] \"spam@spam.com\" { fileinto \"INBOX.spam\" ; } else { keep ; } Warning That folders have to exist beforehand if sieve should move them. Another example of a sieve filter that forward mails to a different address: Example require [ \"copy\" ]; redirect :copy \"user2@otherdomain.tld\" ; Just forward all incoming emails and do not save them locally: Example redirect \"user2@otherdomain.tld\" ; You can also use external programs to filter or pipe (process) messages by adding executable scripts in config/sieve-pipe or config/sieve-filter . This can be used in lieu of a local alias file, for instance to forward an email to a webservice. These programs can then be referenced by filename, by all users. Note that the process running the scripts run as a privileged user. For further information see Dovecot's wiki . require [ \"vnd.dovecot.pipe\" ]; pipe \"external-program\" ; For more examples or a detailed description of the Sieve language have a look at the official site . Other resources are available on the internet where you can find several examples .","title":"User-Defined Sieve Filters"},{"location":"config/advanced/mail-sieve/#manage-sieve","text":"The Manage Sieve extension allows users to modify their Sieve script by themselves. The authentication mechanisms are the same as for the main dovecot service. ManageSieve runs on port 4190 and needs to be enabled using the ENABLE_MANAGESIEVE=1 environment variable. Example # docker-compose.yml ports : - \"4190:4190\" environment : - ENABLE_MANAGESIEVE=1 All user defined sieve scripts that are managed by ManageSieve are stored in the user's home folder in /var/mail/domain.com/user1/sieve . Just one sieve script might be active for a user and is sym-linked to /var/mail/domain.com/user1/.dovecot.sieve automatically. Note ManageSieve makes sure to not overwrite an existing .dovecot.sieve file. If a user activates a new sieve script the old one is backuped and moved to the sieve folder. The extension is known to work with the following ManageSieve clients: Sieve Editor a portable standalone application based on the former Thunderbird plugin. Kmail the mail client of KDE 's Kontact Suite.","title":"Manage Sieve"},{"location":"config/advanced/optional-config/","text":"This is a list of all configuration files and directories which are optional or automatically generated in your config directory. Directories sieve-filter: directory for sieve filter scripts. (Docs: Sieve ) sieve-pipe: directory for sieve pipe scripts. (Docs: Sieve ) opendkim: DKIM directory. Auto-configurable via setup.sh config dkim . (Docs: DKIM ) ssl: SSL Certificate directory. (Docs: SSL ) Files {user_email_address}.dovecot.sieve: User specific Sieve filter file. (Docs: Sieve ) before.dovecot.sieve: Global Sieve filter file, applied prior to the ${login}.dovecot.sieve filter. (Docs: Sieve ) after.dovecot.sieve : Global Sieve filter file, applied after the ${login}.dovecot.sieve filter. (Docs: Sieve ) postfix-main.cf: Every line will be added to the postfix main configuration. (Docs: Override Postfix Defaults ) postfix-master.cf: Every line will be added to the postfix master configuration. (Docs: Override Postfix Defaults ) postfix-accounts.cf: User accounts file. Modify via the setup.sh email script. postfix-send-access.cf: List of users denied sending. Modify via setup.sh email restrict . postfix-receive-access.cf: List of users denied receiving. Modify via setup.sh email restrict . postfix-virtual.cf: Alias configuration file. Modify via setup.sh alias . postfix-sasl-password.cf: listing of relayed domains with their respective : . Modify via setup.sh relay add-auth [] . (Docs: Relay-Hosts Auth ) postfix-relaymap.cf: domain-specific relays and exclusions. Modify via setup.sh relay add-domain and setup.sh relay exclude-domain . (Docs: Relay-Hosts Senders ) postfix-regexp.cf: Regular expression alias file. (Docs: Aliases ) ldap-users.cf: Configuration for the virtual user mapping virtual_mailbox_maps . See the setup-stack.sh script. ldap-groups.cf: Configuration for the virtual alias mapping virtual_alias_maps . See the setup-stack.sh script. ldap-aliases.cf: Configuration for the virtual alias mapping virtual_alias_maps . See the setup-stack.sh script. ldap-domains.cf: Configuration for the virtual domain mapping virtual_mailbox_domains . See the setup-stack.sh script. whitelist_clients.local: Whitelisted domains, not considered by postgrey. Enter one host or domain per line. spamassassin-rules.cf: Antispam rules for Spamassassin. (Docs: FAQ - SpamAssassin Rules ) fail2ban-fail2ban.cf: Additional config options for fail2ban.cf . (Docs: Fail2Ban ) fail2ban-jail.cf: Additional config options for fail2ban's jail behaviour. (Docs: Fail2Ban ) amavis.cf: replaces the /etc/amavis/conf.d/50-user file dovecot.cf: replaces /etc/dovecot/local.conf . (Docs: Override Dovecot Defaults ) dovecot-quotas.cf: list of custom quotas per mailbox. (Docs: Accounts ) user-patches.sh: this file will be run after all configuration files are set up, but before the postfix, amavis and other daemons are started. (Docs: FAQ - How to adjust settings with the user-patches.sh script )","title":"Optional Configuration"},{"location":"config/advanced/optional-config/#directories","text":"sieve-filter: directory for sieve filter scripts. (Docs: Sieve ) sieve-pipe: directory for sieve pipe scripts. (Docs: Sieve ) opendkim: DKIM directory. Auto-configurable via setup.sh config dkim . (Docs: DKIM ) ssl: SSL Certificate directory. (Docs: SSL )","title":"Directories"},{"location":"config/advanced/optional-config/#files","text":"{user_email_address}.dovecot.sieve: User specific Sieve filter file. (Docs: Sieve ) before.dovecot.sieve: Global Sieve filter file, applied prior to the ${login}.dovecot.sieve filter. (Docs: Sieve ) after.dovecot.sieve : Global Sieve filter file, applied after the ${login}.dovecot.sieve filter. (Docs: Sieve ) postfix-main.cf: Every line will be added to the postfix main configuration. (Docs: Override Postfix Defaults ) postfix-master.cf: Every line will be added to the postfix master configuration. (Docs: Override Postfix Defaults ) postfix-accounts.cf: User accounts file. Modify via the setup.sh email script. postfix-send-access.cf: List of users denied sending. Modify via setup.sh email restrict . postfix-receive-access.cf: List of users denied receiving. Modify via setup.sh email restrict . postfix-virtual.cf: Alias configuration file. Modify via setup.sh alias . postfix-sasl-password.cf: listing of relayed domains with their respective : . Modify via setup.sh relay add-auth [] . (Docs: Relay-Hosts Auth ) postfix-relaymap.cf: domain-specific relays and exclusions. Modify via setup.sh relay add-domain and setup.sh relay exclude-domain . (Docs: Relay-Hosts Senders ) postfix-regexp.cf: Regular expression alias file. (Docs: Aliases ) ldap-users.cf: Configuration for the virtual user mapping virtual_mailbox_maps . See the setup-stack.sh script. ldap-groups.cf: Configuration for the virtual alias mapping virtual_alias_maps . See the setup-stack.sh script. ldap-aliases.cf: Configuration for the virtual alias mapping virtual_alias_maps . See the setup-stack.sh script. ldap-domains.cf: Configuration for the virtual domain mapping virtual_mailbox_domains . See the setup-stack.sh script. whitelist_clients.local: Whitelisted domains, not considered by postgrey. Enter one host or domain per line. spamassassin-rules.cf: Antispam rules for Spamassassin. (Docs: FAQ - SpamAssassin Rules ) fail2ban-fail2ban.cf: Additional config options for fail2ban.cf . (Docs: Fail2Ban ) fail2ban-jail.cf: Additional config options for fail2ban's jail behaviour. (Docs: Fail2Ban ) amavis.cf: replaces the /etc/amavis/conf.d/50-user file dovecot.cf: replaces /etc/dovecot/local.conf . (Docs: Override Dovecot Defaults ) dovecot-quotas.cf: list of custom quotas per mailbox. (Docs: Accounts ) user-patches.sh: this file will be run after all configuration files are set up, but before the postfix, amavis and other daemons are started. (Docs: FAQ - How to adjust settings with the user-patches.sh script )","title":"Files"},{"location":"config/advanced/mail-forwarding/aws-ses/","text":"Warning New configuration, see Configure Relay Hosts Instead of letting postfix deliver mail directly it is possible to configure it to deliver outgoing email via Amazon SES (Simple Email Service). (Receiving inbound email via SES is not implemented.) The configuration follows the guidelines provided by AWS in https://docs.aws.amazon.com/ses/latest/DeveloperGuide/postfix.html , specifically, the STARTTLS method. As described in the AWS Developer Guide you will have to generate SMTP credentials and define the following two environment variables in the docker-compose.yml with the appropriate values for your AWS SES subscription (the values for AWS_SES_USERPASS are the \"SMTP username\" and \"SMTP password\" provided when you create SMTP credentials for SES): environment : - AWS_SES_HOST=email-smtp.us-east-1.amazonaws.com - AWS_SES_USERPASS=AKIAXXXXXXXXXXXXXXXX:kqXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX If necessary, you can also provide AWS_SES_PORT . If not provided, it defaults to 25. When you start the container you will see a log line as follows confirming the configuration: Setting up outgoing email via AWS SES host email-smtp.us-east-1.amazonaws.com To verify proper operation, send an email to some external account of yours and inspect the mail headers. You will also see the connection to SES in the mail logs. For example: May 23 07:09:36 mail postfix/smtp[692]: Trusted TLS connection established to email-smtp.us-east-1.amazonaws.com[107.20.142.169]:25: TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits) May 23 07:09:36 mail postfix/smtp[692]: 8C82A7E7: to=, relay=email-smtp.us-east-1.amazonaws.com[107.20.142.169]:25, delay=0.35, delays=0/0.02/0.13/0.2, dsn=2.0.0, status=sent (250 Ok 01000154dc729264-93fdd7ea-f039-43d6-91ed-653e8547867c-000000)","title":"AWS SES"},{"location":"config/advanced/mail-forwarding/relay-hosts/","text":"Introduction Rather than having Postfix deliver mail directly, you can configure Postfix to send mail via another mail relay (smarthost). Examples include Mailgun , Sendgrid and AWS SES . Depending on the domain of the sender, you may want to send via a different relay, or authenticate in a different way. Basic Configuration Basic configuration is done via environment variables: RELAY_HOST : default host to relay mail through, empty will disable this feature RELAY_PORT : port on default relay, defaults to port 25 RELAY_USER : username for the default relay RELAY_PASSWORD : password for the default user Setting these environment variables will cause mail for all sender domains to be routed via the specified host, authenticating with the user/password combination. Warning For users of the previous AWS_SES_* variables: please update your configuration to use these new variables, no other configuration is required. Advanced Configuration Sender-dependent Authentication Sender dependent authentication is done in config/postfix-sasl-password.cf . You can create this file manually, or use: setup.sh relay add-auth [ ] An example configuration file looks like this: @domain1.com relay_user_1:password_1 @domain2.com relay_user_2:password_2 If there is no other configuration, this will cause Postfix to deliver email throught the relay specified in RELAY_HOST env variable, authenticating as relay_user_1 when sent from domain1.com and authenticating as relay_user_2 when sending from domain2.com. Note To activate the configuration you must either restart the container, or you can also trigger an update by modifying a mail account. Sender-dependent Relay Host Sender dependent relay hosts are configured in config/postfix-relaymap.cf . You can create this file manually, or use: setup.sh relay add-domain [ ] An example configuration file looks like this: @domain1.com [relay1.org]:587 @domain2.com [relay2.org]:2525 Combined with the previous configuration in config/postfix-sasl-password.cf , this will cause Postfix to deliver mail sent from domain1.com via relay1.org:587 , authenticating as relay_user_1 , and mail sent from domain2.com via relay2.org:2525 authenticating as relay_user_2 . Note You still have to define RELAY_HOST to activate the feature Excluding Sender Domains If you want mail sent from some domains to be delivered directly, you can exclude them from being delivered via the default relay by adding them to config/postfix-relaymap.cf with no destination. You can also do this via: setup.sh relay exclude-domain Extending the configuration file from above: @domain1.com [relay1.org]:587 @domain2.com [relay2.org]:2525 @domain3.com This will cause email sent from domain3.com to be delivered directly. References Thanks to the author of this article for the inspiration. This is also worth reading to understand a bit more about how to set up Mailgun to work with this.","title":"Relay Hosts"},{"location":"config/advanced/mail-forwarding/relay-hosts/#introduction","text":"Rather than having Postfix deliver mail directly, you can configure Postfix to send mail via another mail relay (smarthost). Examples include Mailgun , Sendgrid and AWS SES . Depending on the domain of the sender, you may want to send via a different relay, or authenticate in a different way.","title":"Introduction"},{"location":"config/advanced/mail-forwarding/relay-hosts/#basic-configuration","text":"Basic configuration is done via environment variables: RELAY_HOST : default host to relay mail through, empty will disable this feature RELAY_PORT : port on default relay, defaults to port 25 RELAY_USER : username for the default relay RELAY_PASSWORD : password for the default user Setting these environment variables will cause mail for all sender domains to be routed via the specified host, authenticating with the user/password combination. Warning For users of the previous AWS_SES_* variables: please update your configuration to use these new variables, no other configuration is required.","title":"Basic Configuration"},{"location":"config/advanced/mail-forwarding/relay-hosts/#advanced-configuration","text":"","title":"Advanced Configuration"},{"location":"config/advanced/mail-forwarding/relay-hosts/#sender-dependent-authentication","text":"Sender dependent authentication is done in config/postfix-sasl-password.cf . You can create this file manually, or use: setup.sh relay add-auth [ ] An example configuration file looks like this: @domain1.com relay_user_1:password_1 @domain2.com relay_user_2:password_2 If there is no other configuration, this will cause Postfix to deliver email throught the relay specified in RELAY_HOST env variable, authenticating as relay_user_1 when sent from domain1.com and authenticating as relay_user_2 when sending from domain2.com. Note To activate the configuration you must either restart the container, or you can also trigger an update by modifying a mail account.","title":"Sender-dependent Authentication"},{"location":"config/advanced/mail-forwarding/relay-hosts/#sender-dependent-relay-host","text":"Sender dependent relay hosts are configured in config/postfix-relaymap.cf . You can create this file manually, or use: setup.sh relay add-domain [ ] An example configuration file looks like this: @domain1.com [relay1.org]:587 @domain2.com [relay2.org]:2525 Combined with the previous configuration in config/postfix-sasl-password.cf , this will cause Postfix to deliver mail sent from domain1.com via relay1.org:587 , authenticating as relay_user_1 , and mail sent from domain2.com via relay2.org:2525 authenticating as relay_user_2 . Note You still have to define RELAY_HOST to activate the feature","title":"Sender-dependent Relay Host"},{"location":"config/advanced/mail-forwarding/relay-hosts/#excluding-sender-domains","text":"If you want mail sent from some domains to be delivered directly, you can exclude them from being delivered via the default relay by adding them to config/postfix-relaymap.cf with no destination. You can also do this via: setup.sh relay exclude-domain Extending the configuration file from above: @domain1.com [relay1.org]:587 @domain2.com [relay2.org]:2525 @domain3.com This will cause email sent from domain3.com to be delivered directly.","title":"Excluding Sender Domains"},{"location":"config/advanced/mail-forwarding/relay-hosts/#references","text":"Thanks to the author of this article for the inspiration. This is also worth reading to understand a bit more about how to set up Mailgun to work with this.","title":"References"},{"location":"config/advanced/maintenance/update-and-cleanup/","text":"Automatic Update Docker images are handy but it can get a a hassle to keep them updated. Also when a repository is automated you want to get these images when they get out. One could setup a complex action/hook-based workflow using probes, but there is a nice, easy to use docker image that solves this issue and could prove useful: watchtower . A docker-compose example: services : watchtower : restart : always image : containrrr/watchtower:latest volumes : - /var/run/docker.sock:/var/run/docker.sock For more details, see the manual Automatic Cleanup When you are pulling new images in automatically, it would be nice to have them cleaned up as well. There is also a docker image for this: spotify/docker-gc . A docker-compose example: services : docker-gc : restart : always image : spotify/docker-gc:latest volumes : - /var/run/docker.sock:/var/run/docker.sock For more details, see the manual Or you can just use the --cleanup option provided by containrrr/watchtower .","title":"Update and Cleanup"},{"location":"config/advanced/maintenance/update-and-cleanup/#automatic-update","text":"Docker images are handy but it can get a a hassle to keep them updated. Also when a repository is automated you want to get these images when they get out. One could setup a complex action/hook-based workflow using probes, but there is a nice, easy to use docker image that solves this issue and could prove useful: watchtower . A docker-compose example: services : watchtower : restart : always image : containrrr/watchtower:latest volumes : - /var/run/docker.sock:/var/run/docker.sock For more details, see the manual","title":"Automatic Update"},{"location":"config/advanced/maintenance/update-and-cleanup/#automatic-cleanup","text":"When you are pulling new images in automatically, it would be nice to have them cleaned up as well. There is also a docker image for this: spotify/docker-gc . A docker-compose example: services : docker-gc : restart : always image : spotify/docker-gc:latest volumes : - /var/run/docker.sock:/var/run/docker.sock For more details, see the manual Or you can just use the --cleanup option provided by containrrr/watchtower .","title":"Automatic Cleanup"},{"location":"config/advanced/override-defaults/dovecot/","text":"Add Configuration The Dovecot default configuration can easily be extended providing a config/dovecot.cf file. Dovecot documentation remains the best place to find configuration options. Your docker-mailserver folder should look like this example: \u251c\u2500\u2500 config \u2502 \u251c\u2500\u2500 dovecot.cf \u2502 \u251c\u2500\u2500 postfix-accounts.cf \u2502 \u2514\u2500\u2500 postfix-virtual.cf \u251c\u2500\u2500 docker-compose.yml \u2514\u2500\u2500 README.md One common option to change is the maximum number of connections per user: mail_max_userip_connections = 100 Another important option is the default_process_limit (defaults to 100 ). If high-security mode is enabled you'll need to make sure this count is higher than the maximum number of users that can be logged in simultaneously. This limit is quickly reached if users connect to the mail server with multiple end devices. Override Configuration For major configuration changes it\u2019s best to override the dovecot configuration files. For each configuration file you want to override, add a list entry under the volumes key. You will need to first obtain the configuration from the running container: mkdir -p ./config/dovecot && docker cp mailserver:/etc/dovecot/conf.d/10-master.conf ./config/dovecot/10-master.conf services : mail : volumes : - maildata:/var/mail - ./config/dovecot/10-master.conf:/etc/dovecot/conf.d/10-master.conf Debugging To debug your dovecot configuration you can use: This command: ./setup.sh debug login doveconf | grep Or: docker exec -it mailserver doveconf | grep Note setup.sh is included in the docker-mailserver repository. Make sure to grap the one matching your image version. The config/dovecot.cf is copied internally to /etc/dovecot/local.conf . To check this file run: docker exec -it mailserver cat /etc/dovecot/local.conf","title":"Dovecot"},{"location":"config/advanced/override-defaults/dovecot/#add-configuration","text":"The Dovecot default configuration can easily be extended providing a config/dovecot.cf file. Dovecot documentation remains the best place to find configuration options. Your docker-mailserver folder should look like this example: \u251c\u2500\u2500 config \u2502 \u251c\u2500\u2500 dovecot.cf \u2502 \u251c\u2500\u2500 postfix-accounts.cf \u2502 \u2514\u2500\u2500 postfix-virtual.cf \u251c\u2500\u2500 docker-compose.yml \u2514\u2500\u2500 README.md One common option to change is the maximum number of connections per user: mail_max_userip_connections = 100 Another important option is the default_process_limit (defaults to 100 ). If high-security mode is enabled you'll need to make sure this count is higher than the maximum number of users that can be logged in simultaneously. This limit is quickly reached if users connect to the mail server with multiple end devices.","title":"Add Configuration"},{"location":"config/advanced/override-defaults/dovecot/#override-configuration","text":"For major configuration changes it\u2019s best to override the dovecot configuration files. For each configuration file you want to override, add a list entry under the volumes key. You will need to first obtain the configuration from the running container: mkdir -p ./config/dovecot && docker cp mailserver:/etc/dovecot/conf.d/10-master.conf ./config/dovecot/10-master.conf services : mail : volumes : - maildata:/var/mail - ./config/dovecot/10-master.conf:/etc/dovecot/conf.d/10-master.conf","title":"Override Configuration"},{"location":"config/advanced/override-defaults/dovecot/#debugging","text":"To debug your dovecot configuration you can use: This command: ./setup.sh debug login doveconf | grep Or: docker exec -it mailserver doveconf | grep Note setup.sh is included in the docker-mailserver repository. Make sure to grap the one matching your image version. The config/dovecot.cf is copied internally to /etc/dovecot/local.conf . To check this file run: docker exec -it mailserver cat /etc/dovecot/local.conf","title":"Debugging"},{"location":"config/advanced/override-defaults/postfix/","text":"The Postfix default configuration can easily be extended by providing a config/postfix-main.cf in postfix format. This can also be used to add configuration that is not in our default configuration. For example, one common use of this file is for increasing the default maximum message size: # increase maximum message size message_size_limit = 52428800 That specific example is now supported and can be handled by setting POSTFIX_MESSAGE_SIZE_LIMIT . Note Postfix documentation remains the best place to find configuration options. Each line in the provided file will be loaded into postfix. In the same way it is possible to add a custom config/postfix-master.cf file that will override the standard master.cf . Each line in the file will be passed to postconf -P . The expected format is // , for example: submission/inet/smtpd_reject_unlisted_recipient = no Run postconf -P in the container without arguments to see the active master options. Note There should be no space between the parameter and the value. Have a look at the code for more information.","title":"Postfix"},{"location":"config/advanced/override-defaults/user-patches/","text":"If you'd like to change, patch or alter files or behavior of docker-mailserver , you can use a script. In case you cloned this repository, you can copy the file user-patches.sh.dist under config/ with cp config/user-patches.sh.dist config/user-patches.sh in order to create the user-patches.sh script. In case you are managing your directory structure yourself, create a config/ directory and the user-patches.sh file yourself. # 1. Either create the config/ directory yourself # or let docker-mailserver create it on initial # startup ~/somewhere $ mkdir config && cd config # 2. Create the user-patches.sh and edit it ~/somewhere/config $ touch user-patches.sh ~/somewhere/config $ vi user-patches.sh The contents could look like this #! /bin/bash cat >/etc/amavis/conf.d/50-user << \"END\" use strict ; $undecipherable_subject_tag = undef ; $admin_maps_by_ccat { +CC_UNCHECKED } = undef ; #------------ Do not modify anything below this line ------------- 1 ; # ensure a defined return END ... And you're done. The user patches script runs right before starting daemons. That means, all the other configuration is in place, so the script can make final adjustments. Note Many \"patches\" can already be done with the Docker Compose-/Stack-file. Adding hostnames to /etc/hosts is done with the extra_hosts : section, sysctl commands can be managed with the sysctls : section, etc.","title":"Modifications via Script"},{"location":"config/best-practices/autodiscover/","text":"Email auto-discovery means a client email is able to automagically find out about what ports and security options to use, based on the mail server URL. It can help simplify the tedious / confusing task of adding own's email account for non-tech savvy users. Email clients will search for auto-discoverable settings and prefill almost everything when a user enters its email address There exists autodiscover-email-settings on which provides IMAP/POP/SMTP/LDAP autodiscover capabilities on Microsoft Outlook/Apple Mail, autoconfig capabilities for Thunderbird or kmail and configuration profiles for iOS/Apple Mail.","title":"Auto-discovery"},{"location":"config/best-practices/dkim/","text":"DKIM is a security measure targeting email spoofing. It is greatly recommended one activates it. Note See the Wikipedia page for more details on DKIM. Enabling DKIM Signature To enable DKIM signature, you must have created at least one email account . Once its done, just run the following command to generate the signature: ./setup.sh config dkim After generating DKIM keys, you should restart the mail server. DNS edits may take a few minutes to hours to propagate. The script assumes you're being in the directory where the config/ directory is located. The default keysize when generating the signature is 4096 bits for now. If you need to change it (e.g. your DNS provider limits the size), then provide the size as the first parameter of the command: ./setup.sh config dkim keysize For LDAP systems that do not have any directly created user account you can run the following command (since 8.0.0 ) to generate the signature by additionally providing the desired domain name (if you have multiple domains use the command multiple times or provide a comma-separated list of domains): ./setup.sh config dkim keysize domain [ , ] Now the keys are generated, you can configure your DNS server with DKIM signature, simply by adding a TXT record. If you have direct access to your DNS zone file, then it's only a matter of pasting the content of config/opendkim/keys/domain.tld/mail.txt in your domain.tld.hosts zone. $ dig mail._domainkey.domain.tld TXT --- ;; ANSWER SECTION mail._domainkey. 300 IN TXT \"v=DKIM1; k=rsa; p=AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN\" Configuration using a Web Interface Generate a new record of the type TXT . Paste mail._domainkey the Name txt field. In the Target or Value field fill in v=DKIM1; k=rsa; p=AZERTYUGHJKLMWX... . In TTL (time to live): Time span in seconds. How long the DNS server should cache the TXT record. Save. Note Sometimes the key in config/opendkim/keys/domain.tld/mail.txt can be on multiple lines. If so then you need to concatenate the values in the TXT record: $ dig mail._domainkey.domain.tld TXT --- ;; ANSWER SECTION mail._domainkey. 300 IN TXT \"v=DKIM1; k=rsa; \" \"p=AZERTYUIOPQSDF...\" \"asdfQWERTYUIOPQSDF...\" The target (or value) field must then have all the parts together: v=DKIM1; k=rsa; p=AZERTYUIOPQSDF...asdfQWERTYUIOPQSDF... Verify-Only If you want DKIM to only verify incoming emails, the following version of /etc/opendkim.conf may be useful (right now there is no easy mechanism for installing it other than forking the repo): # This is a simple config file verifying messages only #LogWhy yes Syslog yes SyslogSuccess yes Socket inet:12301@localhost PidFile /var/run/opendkim/opendkim.pid ReportAddress postmaster@my-domain.com SendReports yes Mode v Switch Off DKIM Simply remove the DKIM key by recreating (not just relaunching) the mailserver container. Debugging DKIM-verifer : A add-on for the mail client Thunderbird. You can debug your TXT records with the dig tool. $ dig TXT mail._domainkey.domain.tld --- ; <<>> DiG 9.10.3-P4-Debian <<>> TXT mail._domainkey.domain.tld ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39669 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;mail._domainkey.domain.tld. IN TXT ;; ANSWER SECTION: mail._domainkey.domain.tld. 3600 IN TXT \"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxBSjG6RnWAdU3oOlqsdf2WC0FOUmU8uHVrzxPLW2R3yRBPGLrGO1++yy3tv6kMieWZwEBHVOdefM6uQOQsZ4brahu9lhG8sFLPX4MaKYN/NR6RK4gdjrZu+MYSdfk3THgSbNwIDAQAB\" ;; Query time: 50 msec ;; SERVER: 127.0.1.1#53(127.0.1.1) ;; WHEN: Wed Sep 07 18:22:57 CEST 2016 ;; MSG SIZE rcvd: 310 Key sizes >=4096-bit Keys of 4096 bits could de denied by some mailservers. According to https://tools.ietf.org/html/rfc6376 keys are preferably between 512 and 2048 bits. See issue #1854 .","title":"DKIM"},{"location":"config/best-practices/dkim/#enabling-dkim-signature","text":"To enable DKIM signature, you must have created at least one email account . Once its done, just run the following command to generate the signature: ./setup.sh config dkim After generating DKIM keys, you should restart the mail server. DNS edits may take a few minutes to hours to propagate. The script assumes you're being in the directory where the config/ directory is located. The default keysize when generating the signature is 4096 bits for now. If you need to change it (e.g. your DNS provider limits the size), then provide the size as the first parameter of the command: ./setup.sh config dkim keysize For LDAP systems that do not have any directly created user account you can run the following command (since 8.0.0 ) to generate the signature by additionally providing the desired domain name (if you have multiple domains use the command multiple times or provide a comma-separated list of domains): ./setup.sh config dkim keysize domain [ , ] Now the keys are generated, you can configure your DNS server with DKIM signature, simply by adding a TXT record. If you have direct access to your DNS zone file, then it's only a matter of pasting the content of config/opendkim/keys/domain.tld/mail.txt in your domain.tld.hosts zone. $ dig mail._domainkey.domain.tld TXT --- ;; ANSWER SECTION mail._domainkey. 300 IN TXT \"v=DKIM1; k=rsa; p=AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN/AZERTYUIOPQSDFGHJKLMWXCVBN\"","title":"Enabling DKIM Signature"},{"location":"config/best-practices/dkim/#configuration-using-a-web-interface","text":"Generate a new record of the type TXT . Paste mail._domainkey the Name txt field. In the Target or Value field fill in v=DKIM1; k=rsa; p=AZERTYUGHJKLMWX... . In TTL (time to live): Time span in seconds. How long the DNS server should cache the TXT record. Save. Note Sometimes the key in config/opendkim/keys/domain.tld/mail.txt can be on multiple lines. If so then you need to concatenate the values in the TXT record: $ dig mail._domainkey.domain.tld TXT --- ;; ANSWER SECTION mail._domainkey. 300 IN TXT \"v=DKIM1; k=rsa; \" \"p=AZERTYUIOPQSDF...\" \"asdfQWERTYUIOPQSDF...\" The target (or value) field must then have all the parts together: v=DKIM1; k=rsa; p=AZERTYUIOPQSDF...asdfQWERTYUIOPQSDF...","title":"Configuration using a Web Interface"},{"location":"config/best-practices/dkim/#verify-only","text":"If you want DKIM to only verify incoming emails, the following version of /etc/opendkim.conf may be useful (right now there is no easy mechanism for installing it other than forking the repo): # This is a simple config file verifying messages only #LogWhy yes Syslog yes SyslogSuccess yes Socket inet:12301@localhost PidFile /var/run/opendkim/opendkim.pid ReportAddress postmaster@my-domain.com SendReports yes Mode v","title":"Verify-Only"},{"location":"config/best-practices/dkim/#switch-off-dkim","text":"Simply remove the DKIM key by recreating (not just relaunching) the mailserver container.","title":"Switch Off DKIM"},{"location":"config/best-practices/dkim/#debugging","text":"DKIM-verifer : A add-on for the mail client Thunderbird. You can debug your TXT records with the dig tool. $ dig TXT mail._domainkey.domain.tld --- ; <<>> DiG 9.10.3-P4-Debian <<>> TXT mail._domainkey.domain.tld ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39669 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;mail._domainkey.domain.tld. IN TXT ;; ANSWER SECTION: mail._domainkey.domain.tld. 3600 IN TXT \"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxBSjG6RnWAdU3oOlqsdf2WC0FOUmU8uHVrzxPLW2R3yRBPGLrGO1++yy3tv6kMieWZwEBHVOdefM6uQOQsZ4brahu9lhG8sFLPX4MaKYN/NR6RK4gdjrZu+MYSdfk3THgSbNwIDAQAB\" ;; Query time: 50 msec ;; SERVER: 127.0.1.1#53(127.0.1.1) ;; WHEN: Wed Sep 07 18:22:57 CEST 2016 ;; MSG SIZE rcvd: 310 Key sizes >=4096-bit Keys of 4096 bits could de denied by some mailservers. According to https://tools.ietf.org/html/rfc6376 keys are preferably between 512 and 2048 bits. See issue #1854 .","title":"Debugging"},{"location":"config/best-practices/dmarc/","text":"Note DMARC Guide: https://github.com/internetstandards/toolbox-wiki/blob/master/DMARC-how-to.md Enabling DMARC In docker-mailserver , DMARC is pre-configured out-of the box. The only thing you need to do in order to enable it, is to add new TXT entry to your DNS. In contrast with DKIM , DMARC DNS entry does not require any keys, but merely setting the configuration values . You can either handcraft the entry by yourself or use one of available generators (like https://dmarcguide.globalcyberalliance.org/ ). Typically something like this should be good to start with (don't forget to replace @domain.com to your actual domain) _dmarc.domain.com. IN TXT \"v=DMARC1; p=none; rua=mailto:dmarc.report@domain.com; ruf=mailto:dmarc.report@domain.com; sp=none; ri=86400\" Or a bit more strict policies (mind p=quarantine and sp=quarantine ): _dmarc IN TXT \"v=DMARC1; p=quarantine; rua=mailto:dmarc.report@domain.com; ruf=mailto:dmarc.report@domain.com; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; sp=quarantine\" DMARC status is not being displayed instantly in Gmail for instance. If you want to check it directly after DNS entries, you can use some services around the Internet such as https://dmarcguide.globalcyberalliance.org/ or https://ondmarc.redsift.com/ . In other case, email clients will show \"DMARC: PASS\" in ~1 day or so. Reference: #1511","title":"DMARC"},{"location":"config/best-practices/dmarc/#enabling-dmarc","text":"In docker-mailserver , DMARC is pre-configured out-of the box. The only thing you need to do in order to enable it, is to add new TXT entry to your DNS. In contrast with DKIM , DMARC DNS entry does not require any keys, but merely setting the configuration values . You can either handcraft the entry by yourself or use one of available generators (like https://dmarcguide.globalcyberalliance.org/ ). Typically something like this should be good to start with (don't forget to replace @domain.com to your actual domain) _dmarc.domain.com. IN TXT \"v=DMARC1; p=none; rua=mailto:dmarc.report@domain.com; ruf=mailto:dmarc.report@domain.com; sp=none; ri=86400\" Or a bit more strict policies (mind p=quarantine and sp=quarantine ): _dmarc IN TXT \"v=DMARC1; p=quarantine; rua=mailto:dmarc.report@domain.com; ruf=mailto:dmarc.report@domain.com; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; sp=quarantine\" DMARC status is not being displayed instantly in Gmail for instance. If you want to check it directly after DNS entries, you can use some services around the Internet such as https://dmarcguide.globalcyberalliance.org/ or https://ondmarc.redsift.com/ . In other case, email clients will show \"DMARC: PASS\" in ~1 day or so. Reference: #1511","title":"Enabling DMARC"},{"location":"config/best-practices/spf/","text":"From Wikipedia : Quote Sender Policy Framework (SPF) is a simple email-validation system designed to detect email spoofing by providing a mechanism to allow receiving mail exchangers to check that incoming mail from a domain comes from a host authorized by that domain's administrators. The list of authorized sending hosts for a domain is published in the Domain Name System (DNS) records for that domain in the form of a specially formatted TXT record. Email spam and phishing often use forged \"from\" addresses, so publishing and checking SPF records can be considered anti-spam techniques. Note For a more technical review: https://github.com/internetstandards/toolbox-wiki/blob/master/SPF-how-to.md Add a SPF Record To add a SPF record in your DNS, insert the following line in your DNS zone: ; MX record must be declared for SPF to work domain.com. IN MX 1 mail.domain.com. ; SPF record domain.com. IN TXT \"v=spf1 mx ~all\" This enables the Softfail mode for SPF. You could first add this SPF record with a very low TTL. SoftFail is a good setting for getting started and testing, as it lets all email through, with spams tagged as such in the mailbox. After verification, you might want to change your SPF record to v=spf1 mx -all so as to enforce the HardFail policy. See http://www.open-spf.org/SPF_Record_Syntax for more details about SPF policies. In any case, increment the SPF record's TTL to its final value. Backup MX, Secondary MX For whitelisting a IP Address from the SPF test, you can create a config file (see policyd-spf.conf ) and mount that file into /etc/postfix-policyd-spf-python/policyd-spf.conf . Example: Create and edit a policyd-spf.conf file here //config/postfix-policyd-spf.conf : debugLevel = 1 #0(only errors)-4(complete data received) skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 # Preferably use IP-Addresses for whitelist lookups: Whitelist = 192.168.0.0/31,192.168.1.0/30 # Domain_Whitelist = mx1.mybackupmx.com,mx2.mybackupmx.com Then add this line to docker-compose.yml : volumes : - ./config/postfix-policyd-spf.conf:/etc/postfix-policyd-spf-python/policyd-spf.conf","title":"SPF"},{"location":"config/best-practices/spf/#add-a-spf-record","text":"To add a SPF record in your DNS, insert the following line in your DNS zone: ; MX record must be declared for SPF to work domain.com. IN MX 1 mail.domain.com. ; SPF record domain.com. IN TXT \"v=spf1 mx ~all\" This enables the Softfail mode for SPF. You could first add this SPF record with a very low TTL. SoftFail is a good setting for getting started and testing, as it lets all email through, with spams tagged as such in the mailbox. After verification, you might want to change your SPF record to v=spf1 mx -all so as to enforce the HardFail policy. See http://www.open-spf.org/SPF_Record_Syntax for more details about SPF policies. In any case, increment the SPF record's TTL to its final value.","title":"Add a SPF Record"},{"location":"config/best-practices/spf/#backup-mx-secondary-mx","text":"For whitelisting a IP Address from the SPF test, you can create a config file (see policyd-spf.conf ) and mount that file into /etc/postfix-policyd-spf-python/policyd-spf.conf . Example: Create and edit a policyd-spf.conf file here //config/postfix-policyd-spf.conf : debugLevel = 1 #0(only errors)-4(complete data received) skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 # Preferably use IP-Addresses for whitelist lookups: Whitelist = 192.168.0.0/31,192.168.1.0/30 # Domain_Whitelist = mx1.mybackupmx.com,mx2.mybackupmx.com Then add this line to docker-compose.yml : volumes : - ./config/postfix-policyd-spf.conf:/etc/postfix-policyd-spf-python/policyd-spf.conf","title":"Backup MX, Secondary MX"},{"location":"config/security/fail2ban/","text":"Fail2Ban is installed automatically and bans IP addresses for 3 hours after 3 failed attempts in 10 minutes by default. If you want to change this, you can easily edit config/fail2ban-jail.cf . You can do the same with the values from fail2ban.conf , e.g dbpurgeage . In that case you need to edit config/fail2ban-fail2ban.cf . Attention The mail container must be launched with the NET_ADMIN capability in order to be able to install the iptable rules that actually ban IP addresses. Thus either include --cap-add=NET_ADMIN in the docker run commandline or the equivalent docker-compose.yml : cap_add : - NET_ADMIN If you don't you will see errors the form of: iptables -w -X f2b-postfix -- stderr: \"getsockopt failed strangely: Operation not permitted\\niptables v1.4.21: can't initialize iptabl es table `filter': Permission denied (you must be root)\\nPerhaps iptables or your kernel needs to be upgraded.\\niptables v1.4.21: can' t initialize iptables table `filter': Permission denied (you must be root)\\nPerhaps iptables or your kernel needs to be upgraded.\\n\" 2016-06-01 00:53:51,284 fail2ban.action [678]: ERROR iptables -w -D INPUT -p tcp -m multiport --dports smtp,465,submission - j f2b-postfix You can also manage and list the banned IPs with the setup.sh script.","title":"Fail2Ban"},{"location":"config/security/mail_crypt/","text":"Info The Mail crypt plugin is used to secure email messages stored in a Dovecot system. Messages are encrypted before written to storage and decrypted after reading. Both operations are transparent to the user. In case of unauthorized access to the storage backend, the messages will, without access to the decryption keys, be unreadable to the offending party. There can be a single encryption key for the whole system or each user can have a key of their own. The used cryptographical methods are widely used standards and keys are stored in portable formats, when possible. Official Dovecot documentation: https://doc.dovecot.org/configuration_manual/mail_crypt_plugin/ Basic Setup Before you can enable mail_crypt, you'll need to copy out several dovecot/conf.d files to the host (from a running container) and then take the container down: mkdir -p config/dovecot docker cp mailserver:/etc/dovecot/conf.d/20-lmtp.conf config/dovecot/ docker cp mailserver:/etc/dovecot/conf.d/20-imap.conf config/dovecot/ docker cp mailserver:/etc/dovecot/conf.d/20-pop3.conf config/dovecot/ docker-compose down You then need to generate your global EC key . The EC key needs to be available in the container. I prefer to mount a /certs directory into the container: services : mailserver : image : docker.io/mailserver/docker-mailserver:latest volumes : . . . - ./certs/:/certs . . . While you're editing the docker-compose.yml, add the configuration files you copied out: services : mailserver : image : docker.io/mailserver/docker-mailserver:latest volumes : . . . - ./config/dovecot/20-lmtp.conf:/etc/dovecot/conf.d/20-lmtp.conf - ./config/dovecot/20-imap.conf:/etc/dovecot/conf.d/20-imap.conf - ./config/dovecot/20-pop3.conf:/etc/dovecot/conf.d/20-pop3.conf - ./certs/:/certs . . . The mail_crypt plugin, unless you're using a non-standard configuration of docker-mailserver, should be enabled on both lmtp and imap . You'll want to edit three different files: ./config/dovecot/20-lmtp.conf protocol lmtp { mail_plugins = $mail_plugins sieve mail_crypt plugin { mail_crypt_global_private_key = # <-- change this domainname : # <-- change this container_name : mailserver ports : - \"25:25\" - \"143:143\" - \"465:465\" - \"587:587\" - \"993:993\" volumes : - ./mail:/var/mail - ./mail-state:/var/mail-state - ./config/:/tmp/docker-mailserver/ - /mnt/data/nginx/certs/:/etc/letsencrypt/live/:ro cap_add : - NET_ADMIN - SYS_PTRACE restart : always cert-companion : image : nginx environment : - \"VIRTUAL_HOST=\" - \"VIRTUAL_NETWORK=nginx-proxy\" - \"LETSENCRYPT_HOST=\" - \"LETSENCRYPT_EMAIL=\" networks : - proxy-tier restart : always networks : proxy-tier : external : name : nginx-proxy The mail container needs to have the letsencrypt certificate folder mounted as a volume. No further changes are needed. The second container is a dummy-sidecar we need, because the mail-container do not expose any web-ports. Set your ENV variables as you need. ( VIRTUAL_HOST and LETSENCRYPT_HOST are mandandory, see documentation) Example using the Let's Encrypt Certificates on a Synology NAS Version 6.2 and later of the Synology NAS DSM OS now come with an interface to generate and renew letencrypt certificates. Navigation into your DSM control panel and go to Security, then click on the tab Certificate to generate and manage letsencrypt certificates. Amongst other things, you can use these to secure your mail server. DSM locates the generated certificates in a folder below /usr/syno/etc/certificate/_archive/ . Navigate to that folder and note the 6 character random folder name of the certificate you'd like to use. Then, add the following to your docker-compose.yml declaration file: volumes : - /usr/syno/etc/certificate/_archive//:/tmp/ssl environment : - SSL_TYPE=manual - SSL_CERT_PATH=/tmp/ssl/fullchain.pem - SSL_KEY_PATH=/tmp/ssl/privkey.pem DSM-generated letsencrypt certificates get auto-renewed every three months. Caddy If you are using Caddy to renew your certificates, please note that only RSA certificates work. Read #1440 for details. In short for Caddy v1 the Caddyfile should look something like: https://mail.domain.com { tls yourcurrentemail@gmail.com { key_type rsa2048 } } For Caddy v2 you can specify the key_type in your server's global settings, which would end up looking something like this if you're using a Caddyfile : { debug admin localhost:2019 http_port 80 https_port 443 default_sni mywebserver.com key_type rsa4096 } If you are instead using a json config for Caddy v2, you can set it in your site's TLS automation policies: Example Code { \"apps\" : { \"http\" : { \"servers\" : { \"srv0\" : { \"listen\" : [ \":443\" ], \"routes\" : [ { \"match\" : [ { \"host\" : [ \"mail.domain.com\" , ] } ], \"handle\" : [ { \"handler\" : \"subroute\" , \"routes\" : [ { \"handle\" : [ { \"body\" : \"\" , \"handler\" : \"static_response\" } ] } ] } ], \"terminal\" : true }, ] } } }, \"tls\" : { \"automation\" : { \"policies\" : [ { \"subjects\" : [ \"mail.domain.com\" , ], \"key_type\" : \"rsa2048\" , \"issuer\" : { \"email\" : \"email@email.com\" , \"module\" : \"acme\" } }, { \"issuer\" : { \"email\" : \"email@email.com\" , \"module\" : \"acme\" } } ] } } } } The generated certificates can be mounted: volumes : - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.domain.com/mail.domain.com.crt:/etc/letsencrypt/live/mail.domain.com/fullchain.pem - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.domain.com/mail.domain.com.key:/etc/letsencrypt/live/mail.domain.com/privkey.pem EC certificates fail in the TLS handshake: CONNECTED(00000003) 140342221178112:error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure:ssl/record/rec_layer_s3.c:1543:SSL alert number 40 no peer certificate available No client certificate CA names sent Traefik v2 Traefik is an open-source application proxy using the ACME protocol . Traefik can request certificates for domains and subdomains, and it will take care of renewals, challenge negotiations, etc. We strongly recommend to use Traefik 's major version 2. Traefik 's storage format is natively supported if the acme.json store is mounted into the container at /etc/letsencrypt/acme.json . The file is also monitored for changes and will trigger a reload of the mail services. Wild card certificates issued for *.domain.tld are supported. You will then want to use SSL_DOMAIN = domain.tld . Lookup of the certificate domain happens in the following order: ${ SSL_DOMAIN } ${ HOSTNAME } ${ DOMAINNAME } This setup only comes with one caveat: The domain has to be configured on another service for Traefik to actually request it from Let'sEncrypt, i.e. Traefik will not issue a certificate without a service / router demanding it. Example Code Here is an example setup for docker-compose : version : '3.8' services : mailserver : image : docker.io/mailserver/docker-mailserver:latest container_name : mailserver hostname : mail domainname : domain.tld volumes : - /traefik/acme.json:/etc/letsencrypt/acme.json:ro environment : SSL_TYPE : letsencrypt SSL_DOMAIN : mail.example.com\" # for a wildcard certificate, use # SSL_DOMAIN: example.com traefik : image : docker.io/traefik:v2.4.8 ports : - \"80:80\" - \"443:443\" command : - --providers.docker - --entrypoints.http.address=:80 - --entrypoints.http.http.redirections.entryPoint.to=https - --entrypoints.http.http.redirections.entryPoint.scheme=https - --entrypoints.https.address=:443 - --entrypoints.https.http.tls.certResolver=letsencrypt - --certificatesresolvers.letsencrypt.acme.email=admin@domain.tld - --certificatesresolvers.letsencrypt.acme.storage=/acme.json - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http volumes : - /traefik/acme.json:/acme.json - /var/run/docker.sock:/var/run/docker.sock:ro whoami : image : docker.io/traefik/whoami:latest labels : - \"traefik.http.routers.whoami.rule=Host(`mail.domain.tld`)\" Self-Signed Certificates Warning Use self-signed certificates only for testing purposes! This feature requires you to provide the following files into your config/ssl/ directory (internal location: /tmp/docker-mailserver/ssl/ ): ${HOSTNAME}-key.pem ${HOSTNAME}-cert.pem demoCA/cacert.pem Where ${HOSTNAME} is the mailserver FQDN ( hostname ( mail ) + domainname ( example.com ), eg: mail.example.com ). To use the certificate: Add SSL_TYPE=self-signed to your container environment variables. If a matching certificate (files listed above) is found in config/ssl , it will be automatically setup in postfix and dovecot. You just have to place them in config/ssl folder. Generating a self-signed certificate Note Since v10, support in setup.sh for generating a self-signed SSL certificate internally was removed. It is now similar to SSL_TYPE=manual ( except manual does not support verification for a custom CA ), but does not require additional ENV vars for providing the location of cert files. One way to generate self-signed certificates is with Smallstep's step CLI . This is exactly what docker-mailserver does for creating test certificates . For example with the FQDN mail.example.test , you can generate the required files by running: #! /bin/sh mkdir -p demoCA step certificate create \"Smallstep Root CA\" \"demoCA/cacert.pem\" \"demoCA/cakey.pem\" \\ --no-password --insecure \\ --profile root-ca \\ --not-before \"2021-01-01T00:00:00+00:00\" \\ --not-after \"2031-01-01T00:00:00+00:00\" \\ --san \"example.test\" \\ --san \"mail.example.test\" \\ --kty RSA --size 2048 step certificate create \"Smallstep Leaf\" mail.example.test-cert.pem mail.example.test-key.pem \\ --no-password --insecure \\ --profile leaf \\ --ca \"demoCA/cacert.pem\" \\ --ca-key \"demoCA/cakey.pem\" \\ --not-before \"2021-01-01T00:00:00+00:00\" \\ --not-after \"2031-01-01T00:00:00+00:00\" \\ --san \"example.test\" \\ --san \"mail.example.test\" \\ --kty RSA --size 2048 If you'd rather not install the CLI tool locally to run the step commands above; you can save the script above to a file such as generate-certs.sh ( and make it executable chmod +x generate-certs.sh ) in a directory that you want the certs to be placed, then run that script with docker: # --user to keep ownership of the files to your user and group ID docker run --rm -it \\ --user \" $( id -u ) : $( id -g ) \" \\ --volume \" ${ PWD } :/tmp\" \\ --workdir \"/tmp\" \\ --entrypoint \"/tmp/generate-certs.sh\" \\ smallstep/step-ca Custom Certificate Files You can also provide your own certificate files. Add these entries to your docker-compose.yml : volumes : - /etc/ssl:/tmp/ssl:ro environment : - SSL_TYPE=manual - SSL_CERT_PATH=/tmp/ssl/cert/public.crt - SSL_KEY_PATH=/tmp/ssl/private/private.key This will mount the path where your ssl certificates reside as read-only under /tmp/ssl . Then all you have to do is to specify the location of your private key and the certificate. Info You may have to restart your mailserver once the certificates change. Testing a Certificate is Valid From your host: docker exec mail openssl s_client \\ -connect 0 .0.0.0:25 \\ -starttls smtp \\ -CApath /etc/ssl/certs/ Or: docker exec mail openssl s_client \\ -connect 0 .0.0.0:143 \\ -starttls imap \\ -CApath /etc/ssl/certs/ And you should see the certificate chain, the server certificate and: Verify return code: 0 (ok) In addition, to verify certificate dates: docker exec mail openssl s_client \\ -connect 0 .0.0.0:25 \\ -starttls smtp \\ -CApath /etc/ssl/certs/ \\ 2 >/dev/null | openssl x509 -noout -dates Plain-Text Access Warning Not recommended for purposes other than testing. Add this to config/dovecot.cf : ssl = yes disable_plaintext_auth = no These options in conjunction mean: SSL/TLS is offered to the client, but the client isn't required to use it. The client is allowed to login with plaintext authentication even when SSL/TLS isn't enabled on the connection. This is insecure , because the plaintext password is exposed to the internet. Importing Certificates Obtained via Another Source If you have another source for SSL/TLS certificates you can import them into the server via an external script. The external script can be found here: external certificate import script . The steps to follow are these: Transport the new certificates to ./config/ssl ( /tmp/ssl in the container) You should provide fullchain.key and privkey.pem Place the script in ./config/ (or /tmp/docker-mailserver/ inside the container) Make the script executable ( chmod +x tomav-renew-certs.sh ) Run the script: docker exec mail /tmp/docker-mailserver/tomav-renew-certs.sh If an error occurs the script will inform you. If not you will see both postfix and dovecot restart. After the certificates have been loaded you can check the certificate: openssl s_client \\ -servername mail.mydomain.net \\ -connect 192 .168.0.72:465 \\ 2 >/dev/null | openssl x509 # or openssl s_client \\ -servername mail.mydomain.net \\ -connect mail.mydomain.net:465 \\ 2 >/dev/null | openssl x509 Or you can check how long the new certificate is valid with commands like: export SITE_URL = \"mail.mydomain.net\" export SITE_IP_URL = \"192.168.0.72\" # can also be `mail.mydomain.net` export SITE_SSL_PORT = \"993\" # imap port dovecot ##works: check if certificate will expire in two weeks #2 weeks is 1209600 seconds #3 weeks is 1814400 #12 weeks is 7257600 #15 weeks is 9072000 certcheck_2weeks = ` openssl s_client -connect ${ SITE_IP_URL } : ${ SITE_SSL_PORT } \\ -servername ${ SITE_URL } 2 > /dev/null | openssl x509 -noout -checkend 1209600 ` #################################### #notes: output can be #Certificate will not expire #Certificate will expire #################### What does the script that imports the certificates do: Check if there are new certs in the /tmp/ssl folder. Check with the ssl cert fingerprint if they differ from the current certificates. If so it will copy the certs to the right places. And restart postfix and dovecot. You can of course run the script by cron once a week or something. In that way you could automate cert renewal. If you do so it is probably wise to run an automated check on certificate expiry as well. Such a check could look something like this: ## code below will alert if certificate expires in less than two weeks ## please adjust varables! ## make sure the mail -s command works! Test! export SITE_URL = \"mail.mydomain.net\" export SITE_IP_URL = \"192.168.2.72\" # can also be `mail.mydomain.net` export SITE_SSL_PORT = \"993\" # imap port dovecot export ALERT_EMAIL_ADDR = \"bill@gates321boom.com\" certcheck_2weeks = ` openssl s_client -connect ${ SITE_IP_URL } : ${ SITE_SSL_PORT } \\ -servername ${ SITE_URL } 2 > /dev/null | openssl x509 -noout -checkend 1209600 ` #################################### #notes: output can be #Certificate will not expire #Certificate will expire #################### #echo \"certcheck 2 weeks gives $certcheck_2weeks\" ##automated check you might run by cron or something ## does tls/ssl certificate expire within two weeks? if [ \" $certcheck_2weeks \" = \"Certificate will not expire\" ] ; then echo \"all is well, certwatch 2 weeks says $certcheck_2weeks \" else echo \"Cert seems to be expiring pretty soon, within two weeks: $certcheck_2weeks \" echo \"we will send an alert email and log as well\" logger Certwatch: cert $SITE_URL will expire in two weeks echo \"Certwatch: cert $SITE_URL will expire in two weeks\" | mail -s \"cert $SITE_URL expires in two weeks \" $ALERT_EMAIL_ADDR fi","title":"SSL/TLS"},{"location":"config/security/ssl/#lets-encrypt-recommended","text":"To enable Let's Encrypt on your mail server, you have to: Get your certificate using letsencrypt client Add an environment variable SSL_TYPE with value letsencrypt (see docker-compose.yml ) Mount your whole letsencrypt folder to /etc/letsencrypt The certs folder name located in letsencrypt/live/ must be the fqdn of your container responding to the hostname command. The fqdn (full qualified domain name) inside the docker container is built combining the hostname and domainname values of the docker-compose file, eg: services : mailserver : hostname : mail domainname : myserver.tld fqdn : mail.myserver.tld You don't have anything else to do. Enjoy.","title":"Let's Encrypt (Recommended)"},{"location":"config/security/ssl/#example-using-docker-for-lets-encrypt","text":"Make a directory to store your letsencrypt logs and configs. In my case: mkdir -p /home/ubuntu/docker/letsencrypt cd /home/ubuntu/docker/letsencrypt Now get the certificate (modify mail.myserver.tld ) and following the certbot instructions. This will need access to port 80 from the internet, adjust your firewall if needed: docker run --rm -it \\ -v $PWD /log/:/var/log/letsencrypt/ \\ -v $PWD /etc/:/etc/letsencrypt/ \\ -p 80 :80 \\ certbot/certbot certonly --standalone -d mail.myserver.tld You can now mount /home/ubuntu/docker/letsencrypt/etc/ in /etc/letsencrypt of docker-mailserver . To renew your certificate just run (this will need access to port 443 from the internet, adjust your firewall if needed): docker run --rm -it \\ -v $PWD /log/:/var/log/letsencrypt/ \\ -v $PWD /etc/:/etc/letsencrypt/ \\ -p 80 :80 \\ -p 443 :443 \\ certbot/certbot renew","title":"Example using Docker for Let's Encrypt"},{"location":"config/security/ssl/#example-using-docker-nginx-proxy-and-letsencrypt-nginx-proxy-companion","text":"If you are running a web server already, it is non-trivial to generate a Let's Encrypt certificate for your mail server using certbot , because port 80 is already occupied. In the following example, we show how docker-mailserver can be run alongside the docker containers nginx-proxy and letsencrypt-nginx-proxy-companion . There are several ways to start nginx-proxy and letsencrypt-nginx-proxy-companion . Any method should be suitable here. For example start nginx-proxy as in the letsencrypt-nginx-proxy-companion documentation : docker run --detach \\ --name nginx-proxy \\ --restart always \\ --publish 80 :80 \\ --publish 443 :443 \\ --volume /server/letsencrypt/etc:/etc/nginx/certs:ro \\ --volume /etc/nginx/vhost.d \\ --volume /usr/share/nginx/html \\ --volume /var/run/docker.sock:/tmp/docker.sock:ro \\ jwilder/nginx-proxy Then start nginx-proxy-letsencrypt : docker run --detach \\ --name nginx-proxy-letsencrypt \\ --restart always \\ --volume /server/letsencrypt/etc:/etc/nginx/certs:rw \\ --volumes-from nginx-proxy \\ --volume /var/run/docker.sock:/var/run/docker.sock:ro \\ jrcs/letsencrypt-nginx-proxy-companion Start the rest of your web server containers as usual. Start another container for your mail.myserver.tld . This will generate a Let's Encrypt certificate for your domain, which can be used by docker-mailserver . It will also run a web server on port 80 at that address: docker run -d \\ --name webmail \\ -e \"VIRTUAL_HOST=mail.myserver.tld\" \\ -e \"LETSENCRYPT_HOST=mail.myserver.tld\" \\ -e \"LETSENCRYPT_EMAIL=foo@bar.com\" \\ library/nginx You may want to add -e LETSENCRYPT_TEST=true to the above while testing to avoid the Let's Encrypt certificate generation rate limits. Finally, start the mailserver with the docker-compose.yml . Make sure your mount path to the letsencrypt certificates is correct. Inside your /path/to/mailserver/docker-compose.yml (for the mailserver from this repo) make sure volumes look like below example: volumes : - maildata:/var/mail - mailstate:/var/mail-state - ./config/:/tmp/docker-mailserver/ - /server/letsencrypt/etc:/etc/letsencrypt/live Then: /path/to/mailserver/docker-compose up -d mail","title":"Example using Docker, nginx-proxy and letsencrypt-nginx-proxy-companion"},{"location":"config/security/ssl/#example-using-docker-nginx-proxy-and-letsencrypt-nginx-proxy-companion-with-docker-compose","text":"The following docker-compose.yml is the basic setup you need for using letsencrypt-nginx-proxy-companion . It is mainly derived from its own wiki/documenation. Example Code version : \"2\" services : nginx : image : nginx container_name : nginx ports : - 80:80 - 443:443 volumes : - /mnt/data/nginx/htpasswd:/etc/nginx/htpasswd - /mnt/data/nginx/conf.d:/etc/nginx/conf.d - /mnt/data/nginx/vhost.d:/etc/nginx/vhost.d - /mnt/data/nginx/html:/usr/share/nginx/html - /mnt/data/nginx/certs:/etc/nginx/certs:ro networks : - proxy-tier restart : always nginx-gen : image : jwilder/docker-gen container_name : nginx-gen volumes : - /var/run/docker.sock:/tmp/docker.sock:ro - /mnt/data/nginx/templates/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro volumes_from : - nginx entrypoint : /usr/local/bin/docker-gen -notify-sighup nginx -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf restart : always letsencrypt-nginx-proxy-companion : image : jrcs/letsencrypt-nginx-proxy-companion container_name : letsencrypt-companion volumes_from : - nginx volumes : - /var/run/docker.sock:/var/run/docker.sock:ro - /mnt/data/nginx/certs:/etc/nginx/certs:rw environment : - NGINX_DOCKER_GEN_CONTAINER=nginx-gen - DEBUG=false restart : always networks : proxy-tier : external : name : nginx-proxy The second part of the setup is the actual mail container. So, in another folder, create another docker-compose.yml with the following content (Removed all ENV variables for this example): Example Code version : '2' services : mailserver : image : mailserver/docker-mailserver:latest hostname : # <-- change this domainname : # <-- change this container_name : mailserver ports : - \"25:25\" - \"143:143\" - \"465:465\" - \"587:587\" - \"993:993\" volumes : - ./mail:/var/mail - ./mail-state:/var/mail-state - ./config/:/tmp/docker-mailserver/ - /mnt/data/nginx/certs/:/etc/letsencrypt/live/:ro cap_add : - NET_ADMIN - SYS_PTRACE restart : always cert-companion : image : nginx environment : - \"VIRTUAL_HOST=\" - \"VIRTUAL_NETWORK=nginx-proxy\" - \"LETSENCRYPT_HOST=\" - \"LETSENCRYPT_EMAIL=\" networks : - proxy-tier restart : always networks : proxy-tier : external : name : nginx-proxy The mail container needs to have the letsencrypt certificate folder mounted as a volume. No further changes are needed. The second container is a dummy-sidecar we need, because the mail-container do not expose any web-ports. Set your ENV variables as you need. ( VIRTUAL_HOST and LETSENCRYPT_HOST are mandandory, see documentation)","title":"Example using Docker, nginx-proxy and letsencrypt-nginx-proxy-companion with docker-compose"},{"location":"config/security/ssl/#example-using-the-lets-encrypt-certificates-on-a-synology-nas","text":"Version 6.2 and later of the Synology NAS DSM OS now come with an interface to generate and renew letencrypt certificates. Navigation into your DSM control panel and go to Security, then click on the tab Certificate to generate and manage letsencrypt certificates. Amongst other things, you can use these to secure your mail server. DSM locates the generated certificates in a folder below /usr/syno/etc/certificate/_archive/ . Navigate to that folder and note the 6 character random folder name of the certificate you'd like to use. Then, add the following to your docker-compose.yml declaration file: volumes : - /usr/syno/etc/certificate/_archive//:/tmp/ssl environment : - SSL_TYPE=manual - SSL_CERT_PATH=/tmp/ssl/fullchain.pem - SSL_KEY_PATH=/tmp/ssl/privkey.pem DSM-generated letsencrypt certificates get auto-renewed every three months.","title":"Example using the Let's Encrypt Certificates on a Synology NAS"},{"location":"config/security/ssl/#caddy","text":"If you are using Caddy to renew your certificates, please note that only RSA certificates work. Read #1440 for details. In short for Caddy v1 the Caddyfile should look something like: https://mail.domain.com { tls yourcurrentemail@gmail.com { key_type rsa2048 } } For Caddy v2 you can specify the key_type in your server's global settings, which would end up looking something like this if you're using a Caddyfile : { debug admin localhost:2019 http_port 80 https_port 443 default_sni mywebserver.com key_type rsa4096 } If you are instead using a json config for Caddy v2, you can set it in your site's TLS automation policies: Example Code { \"apps\" : { \"http\" : { \"servers\" : { \"srv0\" : { \"listen\" : [ \":443\" ], \"routes\" : [ { \"match\" : [ { \"host\" : [ \"mail.domain.com\" , ] } ], \"handle\" : [ { \"handler\" : \"subroute\" , \"routes\" : [ { \"handle\" : [ { \"body\" : \"\" , \"handler\" : \"static_response\" } ] } ] } ], \"terminal\" : true }, ] } } }, \"tls\" : { \"automation\" : { \"policies\" : [ { \"subjects\" : [ \"mail.domain.com\" , ], \"key_type\" : \"rsa2048\" , \"issuer\" : { \"email\" : \"email@email.com\" , \"module\" : \"acme\" } }, { \"issuer\" : { \"email\" : \"email@email.com\" , \"module\" : \"acme\" } } ] } } } } The generated certificates can be mounted: volumes : - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.domain.com/mail.domain.com.crt:/etc/letsencrypt/live/mail.domain.com/fullchain.pem - ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.domain.com/mail.domain.com.key:/etc/letsencrypt/live/mail.domain.com/privkey.pem EC certificates fail in the TLS handshake: CONNECTED(00000003) 140342221178112:error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure:ssl/record/rec_layer_s3.c:1543:SSL alert number 40 no peer certificate available No client certificate CA names sent","title":"Caddy"},{"location":"config/security/ssl/#traefik-v2","text":"Traefik is an open-source application proxy using the ACME protocol . Traefik can request certificates for domains and subdomains, and it will take care of renewals, challenge negotiations, etc. We strongly recommend to use Traefik 's major version 2. Traefik 's storage format is natively supported if the acme.json store is mounted into the container at /etc/letsencrypt/acme.json . The file is also monitored for changes and will trigger a reload of the mail services. Wild card certificates issued for *.domain.tld are supported. You will then want to use SSL_DOMAIN = domain.tld . Lookup of the certificate domain happens in the following order: ${ SSL_DOMAIN } ${ HOSTNAME } ${ DOMAINNAME } This setup only comes with one caveat: The domain has to be configured on another service for Traefik to actually request it from Let'sEncrypt, i.e. Traefik will not issue a certificate without a service / router demanding it. Example Code Here is an example setup for docker-compose : version : '3.8' services : mailserver : image : docker.io/mailserver/docker-mailserver:latest container_name : mailserver hostname : mail domainname : domain.tld volumes : - /traefik/acme.json:/etc/letsencrypt/acme.json:ro environment : SSL_TYPE : letsencrypt SSL_DOMAIN : mail.example.com\" # for a wildcard certificate, use # SSL_DOMAIN: example.com traefik : image : docker.io/traefik:v2.4.8 ports : - \"80:80\" - \"443:443\" command : - --providers.docker - --entrypoints.http.address=:80 - --entrypoints.http.http.redirections.entryPoint.to=https - --entrypoints.http.http.redirections.entryPoint.scheme=https - --entrypoints.https.address=:443 - --entrypoints.https.http.tls.certResolver=letsencrypt - --certificatesresolvers.letsencrypt.acme.email=admin@domain.tld - --certificatesresolvers.letsencrypt.acme.storage=/acme.json - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http volumes : - /traefik/acme.json:/acme.json - /var/run/docker.sock:/var/run/docker.sock:ro whoami : image : docker.io/traefik/whoami:latest labels : - \"traefik.http.routers.whoami.rule=Host(`mail.domain.tld`)\"","title":"Traefik v2"},{"location":"config/security/ssl/#self-signed-certificates","text":"Warning Use self-signed certificates only for testing purposes! This feature requires you to provide the following files into your config/ssl/ directory (internal location: /tmp/docker-mailserver/ssl/ ): ${HOSTNAME}-key.pem ${HOSTNAME}-cert.pem demoCA/cacert.pem Where ${HOSTNAME} is the mailserver FQDN ( hostname ( mail ) + domainname ( example.com ), eg: mail.example.com ). To use the certificate: Add SSL_TYPE=self-signed to your container environment variables. If a matching certificate (files listed above) is found in config/ssl , it will be automatically setup in postfix and dovecot. You just have to place them in config/ssl folder.","title":"Self-Signed Certificates"},{"location":"config/security/ssl/#generating-a-self-signed-certificate","text":"Note Since v10, support in setup.sh for generating a self-signed SSL certificate internally was removed. It is now similar to SSL_TYPE=manual ( except manual does not support verification for a custom CA ), but does not require additional ENV vars for providing the location of cert files. One way to generate self-signed certificates is with Smallstep's step CLI . This is exactly what docker-mailserver does for creating test certificates . For example with the FQDN mail.example.test , you can generate the required files by running: #! /bin/sh mkdir -p demoCA step certificate create \"Smallstep Root CA\" \"demoCA/cacert.pem\" \"demoCA/cakey.pem\" \\ --no-password --insecure \\ --profile root-ca \\ --not-before \"2021-01-01T00:00:00+00:00\" \\ --not-after \"2031-01-01T00:00:00+00:00\" \\ --san \"example.test\" \\ --san \"mail.example.test\" \\ --kty RSA --size 2048 step certificate create \"Smallstep Leaf\" mail.example.test-cert.pem mail.example.test-key.pem \\ --no-password --insecure \\ --profile leaf \\ --ca \"demoCA/cacert.pem\" \\ --ca-key \"demoCA/cakey.pem\" \\ --not-before \"2021-01-01T00:00:00+00:00\" \\ --not-after \"2031-01-01T00:00:00+00:00\" \\ --san \"example.test\" \\ --san \"mail.example.test\" \\ --kty RSA --size 2048 If you'd rather not install the CLI tool locally to run the step commands above; you can save the script above to a file such as generate-certs.sh ( and make it executable chmod +x generate-certs.sh ) in a directory that you want the certs to be placed, then run that script with docker: # --user to keep ownership of the files to your user and group ID docker run --rm -it \\ --user \" $( id -u ) : $( id -g ) \" \\ --volume \" ${ PWD } :/tmp\" \\ --workdir \"/tmp\" \\ --entrypoint \"/tmp/generate-certs.sh\" \\ smallstep/step-ca","title":"Generating a self-signed certificate"},{"location":"config/security/ssl/#custom-certificate-files","text":"You can also provide your own certificate files. Add these entries to your docker-compose.yml : volumes : - /etc/ssl:/tmp/ssl:ro environment : - SSL_TYPE=manual - SSL_CERT_PATH=/tmp/ssl/cert/public.crt - SSL_KEY_PATH=/tmp/ssl/private/private.key This will mount the path where your ssl certificates reside as read-only under /tmp/ssl . Then all you have to do is to specify the location of your private key and the certificate. Info You may have to restart your mailserver once the certificates change.","title":"Custom Certificate Files"},{"location":"config/security/ssl/#testing-a-certificate-is-valid","text":"From your host: docker exec mail openssl s_client \\ -connect 0 .0.0.0:25 \\ -starttls smtp \\ -CApath /etc/ssl/certs/ Or: docker exec mail openssl s_client \\ -connect 0 .0.0.0:143 \\ -starttls imap \\ -CApath /etc/ssl/certs/ And you should see the certificate chain, the server certificate and: Verify return code: 0 (ok) In addition, to verify certificate dates: docker exec mail openssl s_client \\ -connect 0 .0.0.0:25 \\ -starttls smtp \\ -CApath /etc/ssl/certs/ \\ 2 >/dev/null | openssl x509 -noout -dates","title":"Testing a Certificate is Valid"},{"location":"config/security/ssl/#plain-text-access","text":"Warning Not recommended for purposes other than testing. Add this to config/dovecot.cf : ssl = yes disable_plaintext_auth = no These options in conjunction mean: SSL/TLS is offered to the client, but the client isn't required to use it. The client is allowed to login with plaintext authentication even when SSL/TLS isn't enabled on the connection. This is insecure , because the plaintext password is exposed to the internet.","title":"Plain-Text Access"},{"location":"config/security/ssl/#importing-certificates-obtained-via-another-source","text":"If you have another source for SSL/TLS certificates you can import them into the server via an external script. The external script can be found here: external certificate import script . The steps to follow are these: Transport the new certificates to ./config/ssl ( /tmp/ssl in the container) You should provide fullchain.key and privkey.pem Place the script in ./config/ (or /tmp/docker-mailserver/ inside the container) Make the script executable ( chmod +x tomav-renew-certs.sh ) Run the script: docker exec mail /tmp/docker-mailserver/tomav-renew-certs.sh If an error occurs the script will inform you. If not you will see both postfix and dovecot restart. After the certificates have been loaded you can check the certificate: openssl s_client \\ -servername mail.mydomain.net \\ -connect 192 .168.0.72:465 \\ 2 >/dev/null | openssl x509 # or openssl s_client \\ -servername mail.mydomain.net \\ -connect mail.mydomain.net:465 \\ 2 >/dev/null | openssl x509 Or you can check how long the new certificate is valid with commands like: export SITE_URL = \"mail.mydomain.net\" export SITE_IP_URL = \"192.168.0.72\" # can also be `mail.mydomain.net` export SITE_SSL_PORT = \"993\" # imap port dovecot ##works: check if certificate will expire in two weeks #2 weeks is 1209600 seconds #3 weeks is 1814400 #12 weeks is 7257600 #15 weeks is 9072000 certcheck_2weeks = ` openssl s_client -connect ${ SITE_IP_URL } : ${ SITE_SSL_PORT } \\ -servername ${ SITE_URL } 2 > /dev/null | openssl x509 -noout -checkend 1209600 ` #################################### #notes: output can be #Certificate will not expire #Certificate will expire #################### What does the script that imports the certificates do: Check if there are new certs in the /tmp/ssl folder. Check with the ssl cert fingerprint if they differ from the current certificates. If so it will copy the certs to the right places. And restart postfix and dovecot. You can of course run the script by cron once a week or something. In that way you could automate cert renewal. If you do so it is probably wise to run an automated check on certificate expiry as well. Such a check could look something like this: ## code below will alert if certificate expires in less than two weeks ## please adjust varables! ## make sure the mail -s command works! Test! export SITE_URL = \"mail.mydomain.net\" export SITE_IP_URL = \"192.168.2.72\" # can also be `mail.mydomain.net` export SITE_SSL_PORT = \"993\" # imap port dovecot export ALERT_EMAIL_ADDR = \"bill@gates321boom.com\" certcheck_2weeks = ` openssl s_client -connect ${ SITE_IP_URL } : ${ SITE_SSL_PORT } \\ -servername ${ SITE_URL } 2 > /dev/null | openssl x509 -noout -checkend 1209600 ` #################################### #notes: output can be #Certificate will not expire #Certificate will expire #################### #echo \"certcheck 2 weeks gives $certcheck_2weeks\" ##automated check you might run by cron or something ## does tls/ssl certificate expire within two weeks? if [ \" $certcheck_2weeks \" = \"Certificate will not expire\" ] ; then echo \"all is well, certwatch 2 weeks says $certcheck_2weeks \" else echo \"Cert seems to be expiring pretty soon, within two weeks: $certcheck_2weeks \" echo \"we will send an alert email and log as well\" logger Certwatch: cert $SITE_URL will expire in two weeks echo \"Certwatch: cert $SITE_URL will expire in two weeks\" | mail -s \"cert $SITE_URL expires in two weeks \" $ALERT_EMAIL_ADDR fi","title":"Importing Certificates Obtained via Another Source"},{"location":"config/security/understanding-the-ports/","text":"Quick Reference Prefer Implicit TLS ports, they're more secure and if you use a Reverse Proxy, should be less hassle (although it's probably wiser to expose these ports directly to docker-mailserver ). Overview of Email Ports Protocol Explicit TLS 1 Implicit TLS Purpose SMTP 25 N/A Transfer 2 ESMTP 587 465 3 Submission POP3 110 995 Retrieval IMAP4 143 993 Retrieval A connection may be secured over TLS when both ends support STARTTLS . On ports 110, 143 and 587, docker-mailserver will reject a connection that cannot be secured. Port 25 is required to support insecure connections. Receives email, docker-mailserver additionally filters for spam and viruses. For submitting email to the server to be sent to third-parties, you should prefer the submission ports(465, 587) - which require authentication. Unless a relay host is configured(eg SendGrid), outgoing email will leave the server via port 25(thus outbound traffic must not be blocked by your provider or firewall). A submission port since 2018 ( RFC 8314 ). Previously a secure variant of port 25. What Ports Should I Use? (SMTP) Flowchart - Mermaid.js source: View in the Live Editor . flowchart LR subgraph your-server [\"Your Server\"] in_25(25) --> server in_465(465) --> server server((\"docker-mailserver
hello@world.com\")) server --- out_25(25) server --- out_465(465) end third-party(\"Third-party
(sending you email)\") ---|\"Receive email for
hello@world.com\"| in_25 subgraph clients [\"Clients (MUA)\"] mua-client(Thunderbird,
Webmail,
Mutt,
etc) mua-service(Backend software
on another server) end clients ---|\"Send email as
hello@world.com\"| in_465 out_25(25) -->|\"Direct
Delivery\"| tin_25 out_465(465) --> relay(\"MTA
Relay Server\") --> tin_25(25) subgraph third-party-server[\"Third-party Server\"] third-party-mta(\"MTA
friend@example.com\") tin_25(25) --> third-party-mta end Inbound Traffic (On the left) Port 25: Think of this like a physical mailbox, it is open to receive email from anyone who wants to. docker-mailserver will actively filter email delivered on this port for spam or viruses and refuse mail from known bad sources. While you could also use this port internally to send email outbound without requiring authentication, you really should prefer the Submission ports(587, 465). Port 465( and 587 ): This is the equivalent of a post office box where you would send email to be delivered on your behalf( docker-mailserver is that metaphorical post office, aka the MTA). Unlike port 25, these two ports are known as the Submission ports and require a valid email account on the server with a password to be able to send email to anyone outside of the server(an MTA you do not control, eg Outlook or Gmail). Prefer port 465 which provides Implicit TLS. Outbound Traffic (On the Right) Port 25: Send the email directly to the given email address MTA as possible. Like your own docker-mailserver port 25, this is the standard port for receiving email on, thus email will almost always arrive to the final MTA on this port. Note that, there may be additional MTAs further in the chain, but this would be the public facing one representing that email address. Port 465( and 587 ): SMTP Relays are a popular choice to hand-off delivery of email through. Services like SendGrid are useful for bulk email(marketing) or when your webhost or ISP are preventing you from using standard ports like port 25 to send out email(which can be abused by spammers). docker-mailserver can serve as a relay too, but the difference between a DIY relay and a professional service is reputation, which is referenced by MTAs you're delivering to such as Outlook, Gmail or others(perhaps another docker-mailserver server!), when deciding if email should be marked as junked or potentially not delivered at all. As a service like SendGrid has a reputation to maintain, relay is restricted to registered users who must authenticate(even on port 25), they do not store email, merely forward it to another MTA which could be delivered on a different port like 25. Explicit vs Implicit TLS Explicit TLS (aka Opportunistic TLS) - Opt-in Encryption Communication on these ports begin in cleartext , indicating support for STARTTLS . If both client and server support STARTTLS the connection will be secured over TLS, otherwise no encryption will be used. Support for STARTTLS is not always implemented correctly, which can lead to leaking credentials(client sending too early) prior to a TLS connection being established. Third-parties such as some ISPs have also been known to intercept the STARTTLS exchange, modifying network traffic to prevent establishing a secure connection. Due to these security concerns, RFC 8314 (Section 4.1) encourages you to prefer Implicit TLS ports where possible . Implicit TLS - Enforced Encryption Communication is always encrypted, avoiding the above mentioned issues with Explicit TLS. You may know of these ports as SMTPS, POP3S, IMAPS , which indicate the protocol in combination with a TLS connection. However, Explicit TLS ports provide the same benefit when STARTTLS is successfully negotiated; Implicit TLS better communicates the improved security to all three protocols (SMTP/POP3/IMAP over Implicit TLS). Additionally, referring to port 465 as SMTPS would be incorrect, as it is a submissions port requiring authentication to proceed via ESMTP , whereas ESMTPS has a different meaning(STARTTLS supported). Port 25 may lack Implicit TLS, but can be configured to be more secure between trusted parties via MTA-STS, STARTTLS Policy List, DNSSEC and DANE. Security Todo This section should provide any related configuration advice, and probably expand on and link to resources about DANE, DNSSEC, MTA-STS and STARTTLS Policy list, with advice on how to configure/setup these added security layers. Todo A related section or page on ciphers used may be useful, although less important for users to be concerned about. TLS connections on mail servers, compared to web browsers Unlike with HTTP where a web browser client communicates directly with the server providing a website, a secure TLS connection as discussed below is not the equivalent safety that HTTPS provides when the transit of email (receiving or sending) is sent through third-parties, as the secure connection is only between two machines, any additional machines (MTAs) between the MUA and the MDA depends on them establishing secure connections between one another successfully. Other machines that facilitate a connection that generally aren't taken into account can exist between a client and server, such as those where your connection passes through your ISP provider are capable of compromising a cleartext connection through interception.","title":"Understanding the Ports"},{"location":"config/security/understanding-the-ports/#quick-reference","text":"Prefer Implicit TLS ports, they're more secure and if you use a Reverse Proxy, should be less hassle (although it's probably wiser to expose these ports directly to docker-mailserver ).","title":"Quick Reference"},{"location":"config/security/understanding-the-ports/#overview-of-email-ports","text":"Protocol Explicit TLS 1 Implicit TLS Purpose SMTP 25 N/A Transfer 2 ESMTP 587 465 3 Submission POP3 110 995 Retrieval IMAP4 143 993 Retrieval A connection may be secured over TLS when both ends support STARTTLS . On ports 110, 143 and 587, docker-mailserver will reject a connection that cannot be secured. Port 25 is required to support insecure connections. Receives email, docker-mailserver additionally filters for spam and viruses. For submitting email to the server to be sent to third-parties, you should prefer the submission ports(465, 587) - which require authentication. Unless a relay host is configured(eg SendGrid), outgoing email will leave the server via port 25(thus outbound traffic must not be blocked by your provider or firewall). A submission port since 2018 ( RFC 8314 ). Previously a secure variant of port 25.","title":"Overview of Email Ports"},{"location":"config/security/understanding-the-ports/#what-ports-should-i-use-smtp","text":"Flowchart - Mermaid.js source: View in the Live Editor . flowchart LR subgraph your-server [\"Your Server\"] in_25(25) --> server in_465(465) --> server server((\"docker-mailserver
hello@world.com\")) server --- out_25(25) server --- out_465(465) end third-party(\"Third-party
(sending you email)\") ---|\"Receive email for
hello@world.com\"| in_25 subgraph clients [\"Clients (MUA)\"] mua-client(Thunderbird,
Webmail,
Mutt,
etc) mua-service(Backend software
on another server) end clients ---|\"Send email as
hello@world.com\"| in_465 out_25(25) -->|\"Direct
Delivery\"| tin_25 out_465(465) --> relay(\"MTA
Relay Server\") --> tin_25(25) subgraph third-party-server[\"Third-party Server\"] third-party-mta(\"MTA
friend@example.com\") tin_25(25) --> third-party-mta end","title":"What Ports Should I Use? (SMTP)"},{"location":"config/security/understanding-the-ports/#inbound-traffic-on-the-left","text":"Port 25: Think of this like a physical mailbox, it is open to receive email from anyone who wants to. docker-mailserver will actively filter email delivered on this port for spam or viruses and refuse mail from known bad sources. While you could also use this port internally to send email outbound without requiring authentication, you really should prefer the Submission ports(587, 465). Port 465( and 587 ): This is the equivalent of a post office box where you would send email to be delivered on your behalf( docker-mailserver is that metaphorical post office, aka the MTA). Unlike port 25, these two ports are known as the Submission ports and require a valid email account on the server with a password to be able to send email to anyone outside of the server(an MTA you do not control, eg Outlook or Gmail). Prefer port 465 which provides Implicit TLS.","title":"Inbound Traffic (On the left)"},{"location":"config/security/understanding-the-ports/#outbound-traffic-on-the-right","text":"Port 25: Send the email directly to the given email address MTA as possible. Like your own docker-mailserver port 25, this is the standard port for receiving email on, thus email will almost always arrive to the final MTA on this port. Note that, there may be additional MTAs further in the chain, but this would be the public facing one representing that email address. Port 465( and 587 ): SMTP Relays are a popular choice to hand-off delivery of email through. Services like SendGrid are useful for bulk email(marketing) or when your webhost or ISP are preventing you from using standard ports like port 25 to send out email(which can be abused by spammers). docker-mailserver can serve as a relay too, but the difference between a DIY relay and a professional service is reputation, which is referenced by MTAs you're delivering to such as Outlook, Gmail or others(perhaps another docker-mailserver server!), when deciding if email should be marked as junked or potentially not delivered at all. As a service like SendGrid has a reputation to maintain, relay is restricted to registered users who must authenticate(even on port 25), they do not store email, merely forward it to another MTA which could be delivered on a different port like 25.","title":"Outbound Traffic (On the Right)"},{"location":"config/security/understanding-the-ports/#explicit-vs-implicit-tls","text":"","title":"Explicit vs Implicit TLS"},{"location":"config/security/understanding-the-ports/#explicit-tls-aka-opportunistic-tls-opt-in-encryption","text":"Communication on these ports begin in cleartext , indicating support for STARTTLS . If both client and server support STARTTLS the connection will be secured over TLS, otherwise no encryption will be used. Support for STARTTLS is not always implemented correctly, which can lead to leaking credentials(client sending too early) prior to a TLS connection being established. Third-parties such as some ISPs have also been known to intercept the STARTTLS exchange, modifying network traffic to prevent establishing a secure connection. Due to these security concerns, RFC 8314 (Section 4.1) encourages you to prefer Implicit TLS ports where possible .","title":"Explicit TLS (aka Opportunistic TLS) - Opt-in Encryption"},{"location":"config/security/understanding-the-ports/#implicit-tls-enforced-encryption","text":"Communication is always encrypted, avoiding the above mentioned issues with Explicit TLS. You may know of these ports as SMTPS, POP3S, IMAPS , which indicate the protocol in combination with a TLS connection. However, Explicit TLS ports provide the same benefit when STARTTLS is successfully negotiated; Implicit TLS better communicates the improved security to all three protocols (SMTP/POP3/IMAP over Implicit TLS). Additionally, referring to port 465 as SMTPS would be incorrect, as it is a submissions port requiring authentication to proceed via ESMTP , whereas ESMTPS has a different meaning(STARTTLS supported). Port 25 may lack Implicit TLS, but can be configured to be more secure between trusted parties via MTA-STS, STARTTLS Policy List, DNSSEC and DANE.","title":"Implicit TLS - Enforced Encryption"},{"location":"config/security/understanding-the-ports/#security","text":"Todo This section should provide any related configuration advice, and probably expand on and link to resources about DANE, DNSSEC, MTA-STS and STARTTLS Policy list, with advice on how to configure/setup these added security layers. Todo A related section or page on ciphers used may be useful, although less important for users to be concerned about.","title":"Security"},{"location":"config/security/understanding-the-ports/#tls-connections-on-mail-servers-compared-to-web-browsers","text":"Unlike with HTTP where a web browser client communicates directly with the server providing a website, a secure TLS connection as discussed below is not the equivalent safety that HTTPS provides when the transit of email (receiving or sending) is sent through third-parties, as the secure connection is only between two machines, any additional machines (MTAs) between the MUA and the MDA depends on them establishing secure connections between one another successfully. Other machines that facilitate a connection that generally aren't taken into account can exist between a client and server, such as those where your connection passes through your ISP provider are capable of compromising a cleartext connection through interception.","title":"TLS connections on mail servers, compared to web browsers"},{"location":"config/troubleshooting/debugging/","text":"Contributions Welcome! Please contribute your solutions to help the community Enable Verbose Debugging Output You may find it useful to enable the DMS_DEBUG environment variable. Invalid Username or Password Shell into the container: docker exec -it bash Check log files in /var/log/mail could not find any mention of incorrect logins here neither in the dovecot logs. Check the supervisors logs in /var/log/supervisor . You can find the logs for startup of fetchmail, postfix and others here - they might indicate problems during startup. Make sure you set your hostname to mail or whatever you specified in your docker-compose.yml file or else your FQDN will be wrong. Installation Errors During setup, if you get errors trying to edit files inside of the container, you likely need to install vi : sudo su docker exec -it apt-get install -y vim Testing Connection I spent HOURS trying to debug \"Connection Refused\" and \"Connection closed by foreign host\" errors when trying to use telnet to troubleshoot my connection. I was also trying to connect from my email client (macOS mail) around the same time. Telnet had also worked earlier, so I was extremely confused as to why it suddenly stopped working. I stumbled upon fail2ban.log in my container. In short, when trying to get my macOS client working, I exceeded the number of failed login attempts and fail2ban put dovecot and postfix in jail! I got around it by whitelisting my ipaddresses (my ec2 instance and my local computer) sudo su docker exec -ti mail bash cd /var/log cat fail2ban.log | grep dovecot # Whitelist IP addresses: fail2ban-client set dovecot addignoreip # Server fail2ban-client set postfix addignoreip fail2ban-client set dovecot addignoreip # Client fail2ban-client set postfix addignoreip # This will delete the jails entirely - nuclear option fail2ban-client stop dovecot fail2ban-client stop postfix Sent email is never received Some hosting provides have a stealth block on port 25. Make sure to check with your hosting provider that traffic on port 25 is allowed Common hosting providers known to have this issue: Azure AWS EC2","title":"Debugging"},{"location":"config/troubleshooting/debugging/#enable-verbose-debugging-output","text":"You may find it useful to enable the DMS_DEBUG environment variable.","title":"Enable Verbose Debugging Output"},{"location":"config/troubleshooting/debugging/#invalid-username-or-password","text":"Shell into the container: docker exec -it bash Check log files in /var/log/mail could not find any mention of incorrect logins here neither in the dovecot logs. Check the supervisors logs in /var/log/supervisor . You can find the logs for startup of fetchmail, postfix and others here - they might indicate problems during startup. Make sure you set your hostname to mail or whatever you specified in your docker-compose.yml file or else your FQDN will be wrong.","title":"Invalid Username or Password"},{"location":"config/troubleshooting/debugging/#installation-errors","text":"During setup, if you get errors trying to edit files inside of the container, you likely need to install vi : sudo su docker exec -it apt-get install -y vim","title":"Installation Errors"},{"location":"config/troubleshooting/debugging/#testing-connection","text":"I spent HOURS trying to debug \"Connection Refused\" and \"Connection closed by foreign host\" errors when trying to use telnet to troubleshoot my connection. I was also trying to connect from my email client (macOS mail) around the same time. Telnet had also worked earlier, so I was extremely confused as to why it suddenly stopped working. I stumbled upon fail2ban.log in my container. In short, when trying to get my macOS client working, I exceeded the number of failed login attempts and fail2ban put dovecot and postfix in jail! I got around it by whitelisting my ipaddresses (my ec2 instance and my local computer) sudo su docker exec -ti mail bash cd /var/log cat fail2ban.log | grep dovecot # Whitelist IP addresses: fail2ban-client set dovecot addignoreip # Server fail2ban-client set postfix addignoreip fail2ban-client set dovecot addignoreip # Client fail2ban-client set postfix addignoreip # This will delete the jails entirely - nuclear option fail2ban-client stop dovecot fail2ban-client stop postfix","title":"Testing Connection"},{"location":"config/troubleshooting/debugging/#sent-email-is-never-received","text":"Some hosting provides have a stealth block on port 25. Make sure to check with your hosting provider that traffic on port 25 is allowed Common hosting providers known to have this issue: Azure AWS EC2","title":"Sent email is never received"},{"location":"config/user-management/accounts/","text":"Adding a New Account Users (email accounts) are managed in /tmp/docker-mailserver/postfix-accounts.cf . The best way to manage accounts is to use the reliable setup.sh script . Or you may directly add the full email address and its encrypted password, separated by a pipe: user1@domain.tld|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1 user2@otherdomain.tld|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1 In the example above, we've added 2 mail accounts for 2 different domains. Consequently, the mail server will automatically be configured for multi-domains. Therefore, to generate a new mail account data, directly from your docker host, you could for example run the following: docker run --rm \\ -e MAIL_USER = user1@domain.tld \\ -e MAIL_PASS = mypassword \\ -it mailserver/docker-mailserver:latest \\ /bin/sh -c 'echo \"$MAIL_USER|$(doveadm pw -s SHA512-CRYPT -u $MAIL_USER -p $MAIL_PASS)\"' >> config/postfix-accounts.cf You will then be asked for a password, and be given back the data for a new account entry, as text. To actually add this new account, just copy all the output text in config/postfix-accounts.cf file of your running container. Note doveadm pw command lets you choose between several encryption schemes for the password. Use doveadm pw -l to get a list of the currently supported encryption schemes. Note Changes to the accounts list require a restart of the container, using supervisord . See #552 . Notes imap-quota is enabled and allow clients to query their mailbox usage. When the mailbox is deleted, the quota directive is deleted as well. Dovecot quotas support LDAP, but it's not implemented ( PR are welcome! ).","title":"Accounts"},{"location":"config/user-management/accounts/#adding-a-new-account","text":"Users (email accounts) are managed in /tmp/docker-mailserver/postfix-accounts.cf . The best way to manage accounts is to use the reliable setup.sh script . Or you may directly add the full email address and its encrypted password, separated by a pipe: user1@domain.tld|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1 user2@otherdomain.tld|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1 In the example above, we've added 2 mail accounts for 2 different domains. Consequently, the mail server will automatically be configured for multi-domains. Therefore, to generate a new mail account data, directly from your docker host, you could for example run the following: docker run --rm \\ -e MAIL_USER = user1@domain.tld \\ -e MAIL_PASS = mypassword \\ -it mailserver/docker-mailserver:latest \\ /bin/sh -c 'echo \"$MAIL_USER|$(doveadm pw -s SHA512-CRYPT -u $MAIL_USER -p $MAIL_PASS)\"' >> config/postfix-accounts.cf You will then be asked for a password, and be given back the data for a new account entry, as text. To actually add this new account, just copy all the output text in config/postfix-accounts.cf file of your running container. Note doveadm pw command lets you choose between several encryption schemes for the password. Use doveadm pw -l to get a list of the currently supported encryption schemes. Note Changes to the accounts list require a restart of the container, using supervisord . See #552 .","title":"Adding a New Account"},{"location":"config/user-management/accounts/#notes","text":"imap-quota is enabled and allow clients to query their mailbox usage. When the mailbox is deleted, the quota directive is deleted as well. Dovecot quotas support LDAP, but it's not implemented ( PR are welcome! ).","title":"Notes"},{"location":"config/user-management/aliases/","text":"Please read the Postfix documentation on virtual aliases first. You can use setup.sh instead of creating and editing files manually. Aliases are managed in /tmp/docker-mailserver/postfix-virtual.cf . An alias is a full email address that will either be: delivered to an existing account registered in /tmp/docker-mailserver/postfix-accounts.cf redirected to one or more other email addresses Alias and target are space separated. An example on a server with domain.tld as its domain: # Alias delivered to an existing account alias1@domain.tld user1@domain.tld # Alias forwarded to an external email address alias2@domain.tld external@gmail.com Configuring RegExp Aliases Additional regexp aliases can be configured by placing them into config/postfix-regexp.cf . The regexp aliases get evaluated after the virtual aliases ( /tmp/docker-mailserver/postfix-virtual.cf ). For example, the following config/postfix-regexp.cf causes all email to \"test\" users to be delivered to qa@example.com : /^test[0-9][0-9]*@example.com/ qa@example.com Address Tags (Extension Delimiters) an Alternative to Aliases Postfix supports so-called address tags, in the form of plus (+) tags - i.e. address+tag@example.com will end up at address@example.com . This is configured by default and the (configurable !) separator is set to + . For more info, see How to use Address Tagging ( user+tag@example.com ) with Postfix and the official documentation . Note If you do decide to change the configurable separator, you must add the same line to both config/postfix-main.cf and config/dovecot.cf , because Dovecot is acting as the delivery agent. For example, to switch to - , add: recipient_delimiter = -","title":"Aliases"},{"location":"config/user-management/aliases/#configuring-regexp-aliases","text":"Additional regexp aliases can be configured by placing them into config/postfix-regexp.cf . The regexp aliases get evaluated after the virtual aliases ( /tmp/docker-mailserver/postfix-virtual.cf ). For example, the following config/postfix-regexp.cf causes all email to \"test\" users to be delivered to qa@example.com : /^test[0-9][0-9]*@example.com/ qa@example.com","title":"Configuring RegExp Aliases"},{"location":"config/user-management/aliases/#address-tags-extension-delimiters-an-alternative-to-aliases","text":"Postfix supports so-called address tags, in the form of plus (+) tags - i.e. address+tag@example.com will end up at address@example.com . This is configured by default and the (configurable !) separator is set to + . For more info, see How to use Address Tagging ( user+tag@example.com ) with Postfix and the official documentation . Note If you do decide to change the configurable separator, you must add the same line to both config/postfix-main.cf and config/dovecot.cf , because Dovecot is acting as the delivery agent. For example, to switch to - , add: recipient_delimiter = -","title":"Address Tags (Extension Delimiters) an Alternative to Aliases"},{"location":"contributing/coding-style/","text":"Bash and Shell When refactoring, writing or altering scripts, that is Shell and bash scripts, in any way, adhere to these rules: Adjust your style of coding to the style that is already present ! Even if you do not like it, this is due to consistency. There was a lot of work involved in making all scripts consistent. Use shellcheck to check your scripts ! Your contributions are checked by GitHub Actions too, so you will need to do this. You can lint your work with make lint to check against all targets. Use the provided .editorconfig file. Use /bin/bash instead of /bin/sh . Adjust the style accordingly. setup.sh provides a good starting point to look for. When appropriate, use the set builtin. We recommend set -euEo pipefail or set -uE . Styling rules If-Else-Statements # when using braces, use double braces # remember you do not need \"\" when using [[ ]] if [[ ]] && [[ -f ${ FILE } ]] then # when running commands, you don't need braces elif else fi # equality checks with numbers are done # with -eq/-ne/-lt/-ge, not != or == if [[ ${ VAR } -ne 42 ]] || [[ ${ SOME_VAR } -eq 6 ]] then fi Variables & Braces Attention Variables are always uppercase. We always use braces. If you forgot this and want to change it later, you can use this link . The used regex is \\$([^{(\"\\\\'\\/])([a-zA-Z0-9_]*)([^}\\/ \\t'\"\\n.\\]:(=\\\\-]*) , where you should in practice be able to replace all variable occurrences without braces with occurrences with braces. # good local VAR = \"good\" local NEW = \" ${ VAR } \" # bad -> CI will fail var = \"bad\" new = $var Loops Like if-else , loops look like this for / while do done Functions It's always nice to see the use of functions as it also provides a clear structure. If scripts are small, this is unnecessary, but if they become larger, please consider using functions. When doing so, provide function _main . function _ { # variables that can be local should be local local } Error Tracing A construct to trace error in your scripts looks like this. Remember: Remove set -x in the end. This is for debugging purposes only. set -xeuEo pipefail trap '__log_err ${FUNCNAME[0]:-\"?\"} ${BASH_COMMAND:-\"?\"} ${LINENO:-\"?\"} ${?:-\"?\"}' ERR SCRIPT = 'name_of_this_script.sh' function __log_err { printf \"\\n\u2013\u2013\u2013 \\e[1m\\e[31mUNCHECKED ERROR\\e[0m\\n%s\\n%s\\n%s\\n%s\\n\\n\" \\ \" \u2013 script = ${ SCRIPT :- ${ 0 }} \" \\ \" \u2013 function = ${ 1 } / ${ 2 } \" \\ \" \u2013 line = ${ 3 } \" \\ \" \u2013 exit code = ${ 4 } \" 1 > & 2 } Comments, Descriptiveness & An Example Comments should only describe non-obvious matters. Comments should start lowercase when they aren't sentences. Make the code self-descriptive by using meaningful names! Make comments not longer than approximately 80 columns, then wrap the line. A positive example, which is taken from start-mailserver.sh , would be function _setup_postfix_aliases { _notify 'task' 'Setting up Postfix Aliases' : >/etc/postfix/virtual : >/etc/postfix/regexp if [[ -f /tmp/docker-mailserver/postfix-virtual.cf ]] then # fixing old virtual user file if grep -q \", $ \" /tmp/docker-mailserver/postfix-virtual.cf then sed -i -e \"s/, /,/g\" -e \"s/, $ //g\" /tmp/docker-mailserver/postfix-virtual.cf fi cp -f /tmp/docker-mailserver/postfix-virtual.cf /etc/postfix/virtual # the `to` is important, don't delete it # shellcheck disable=SC2034 while read -r FROM TO do # Setting variables for better readability UNAME = $( echo \" ${ FROM } \" | cut -d @ -f1 ) DOMAIN = $( echo \" ${ FROM } \" | cut -d @ -f2 ) # if they are equal it means the line looks like: \"user1 other@domain.tld\" [[ \" ${ UNAME } \" ! = \" ${ DOMAIN } \" ]] && echo \" ${ DOMAIN } \" >> /tmp/vhost.tmp done < < ( grep -v \"^\\s* $ \\|^\\s*\\#\" /tmp/docker-mailserver/postfix-virtual.cf || true ) else _notify 'inf' \"Warning 'config/postfix-virtual.cf' is not provided. No mail alias/forward created.\" fi ... } YAML When formatting YAML files, use Prettier , an opinionated formatter. There are many plugins for IDEs around.","title":"Coding Style"},{"location":"contributing/coding-style/#bash-and-shell","text":"When refactoring, writing or altering scripts, that is Shell and bash scripts, in any way, adhere to these rules: Adjust your style of coding to the style that is already present ! Even if you do not like it, this is due to consistency. There was a lot of work involved in making all scripts consistent. Use shellcheck to check your scripts ! Your contributions are checked by GitHub Actions too, so you will need to do this. You can lint your work with make lint to check against all targets. Use the provided .editorconfig file. Use /bin/bash instead of /bin/sh . Adjust the style accordingly. setup.sh provides a good starting point to look for. When appropriate, use the set builtin. We recommend set -euEo pipefail or set -uE .","title":"Bash and Shell"},{"location":"contributing/coding-style/#styling-rules","text":"","title":"Styling rules"},{"location":"contributing/coding-style/#if-else-statements","text":"# when using braces, use double braces # remember you do not need \"\" when using [[ ]] if [[ ]] && [[ -f ${ FILE } ]] then # when running commands, you don't need braces elif else fi # equality checks with numbers are done # with -eq/-ne/-lt/-ge, not != or == if [[ ${ VAR } -ne 42 ]] || [[ ${ SOME_VAR } -eq 6 ]] then fi","title":"If-Else-Statements"},{"location":"contributing/coding-style/#variables-braces","text":"Attention Variables are always uppercase. We always use braces. If you forgot this and want to change it later, you can use this link . The used regex is \\$([^{(\"\\\\'\\/])([a-zA-Z0-9_]*)([^}\\/ \\t'\"\\n.\\]:(=\\\\-]*) , where you should in practice be able to replace all variable occurrences without braces with occurrences with braces. # good local VAR = \"good\" local NEW = \" ${ VAR } \" # bad -> CI will fail var = \"bad\" new = $var","title":"Variables & Braces"},{"location":"contributing/coding-style/#loops","text":"Like if-else , loops look like this for / while do done","title":"Loops"},{"location":"contributing/coding-style/#functions","text":"It's always nice to see the use of functions as it also provides a clear structure. If scripts are small, this is unnecessary, but if they become larger, please consider using functions. When doing so, provide function _main . function _ { # variables that can be local should be local local }","title":"Functions"},{"location":"contributing/coding-style/#error-tracing","text":"A construct to trace error in your scripts looks like this. Remember: Remove set -x in the end. This is for debugging purposes only. set -xeuEo pipefail trap '__log_err ${FUNCNAME[0]:-\"?\"} ${BASH_COMMAND:-\"?\"} ${LINENO:-\"?\"} ${?:-\"?\"}' ERR SCRIPT = 'name_of_this_script.sh' function __log_err { printf \"\\n\u2013\u2013\u2013 \\e[1m\\e[31mUNCHECKED ERROR\\e[0m\\n%s\\n%s\\n%s\\n%s\\n\\n\" \\ \" \u2013 script = ${ SCRIPT :- ${ 0 }} \" \\ \" \u2013 function = ${ 1 } / ${ 2 } \" \\ \" \u2013 line = ${ 3 } \" \\ \" \u2013 exit code = ${ 4 } \" 1 > & 2 }","title":"Error Tracing"},{"location":"contributing/coding-style/#comments-descriptiveness-an-example","text":"Comments should only describe non-obvious matters. Comments should start lowercase when they aren't sentences. Make the code self-descriptive by using meaningful names! Make comments not longer than approximately 80 columns, then wrap the line. A positive example, which is taken from start-mailserver.sh , would be function _setup_postfix_aliases { _notify 'task' 'Setting up Postfix Aliases' : >/etc/postfix/virtual : >/etc/postfix/regexp if [[ -f /tmp/docker-mailserver/postfix-virtual.cf ]] then # fixing old virtual user file if grep -q \", $ \" /tmp/docker-mailserver/postfix-virtual.cf then sed -i -e \"s/, /,/g\" -e \"s/, $ //g\" /tmp/docker-mailserver/postfix-virtual.cf fi cp -f /tmp/docker-mailserver/postfix-virtual.cf /etc/postfix/virtual # the `to` is important, don't delete it # shellcheck disable=SC2034 while read -r FROM TO do # Setting variables for better readability UNAME = $( echo \" ${ FROM } \" | cut -d @ -f1 ) DOMAIN = $( echo \" ${ FROM } \" | cut -d @ -f2 ) # if they are equal it means the line looks like: \"user1 other@domain.tld\" [[ \" ${ UNAME } \" ! = \" ${ DOMAIN } \" ]] && echo \" ${ DOMAIN } \" >> /tmp/vhost.tmp done < < ( grep -v \"^\\s* $ \\|^\\s*\\#\" /tmp/docker-mailserver/postfix-virtual.cf || true ) else _notify 'inf' \"Warning 'config/postfix-virtual.cf' is not provided. No mail alias/forward created.\" fi ... }","title":"Comments, Descriptiveness & An Example"},{"location":"contributing/coding-style/#yaml","text":"When formatting YAML files, use Prettier , an opinionated formatter. There are many plugins for IDEs around.","title":"YAML"},{"location":"contributing/documentation/","text":"Todo This section should provide a detailed step by step guide on how to contribute to documentation","title":"Documentation"},{"location":"contributing/issues-and-pull-requests/","text":"This project is Open Source. That means that you can contribute on enhancements, bug fixing or improving the documentation. Opening an Issue Attention Before opening an issue , read the README carefully, study the documentation , the Postfix/Dovecot documentation and your search engine you trust. The issue tracker is not meant to be used for unrelated questions! When opening an issue, please provide details use case to let the community reproduce your problem. Please start the mail server with env DMS_DEBUG=1 and paste the output into the issue. Attention Use the issue templates to provide the necessary information. Issues which do not use these templates are not worked on and closed. By raising issues, I agree to these terms and I understand, that the rules set for the issue tracker will help both maintainers as well as everyone to find a solution. Maintainers take the time to improve on this project and help by solving issues together. It is therefore expected from others to make an effort and comply with the rules . Pull Requests Submit a Pull-Request Motivation You want to add a feature? Feel free to start creating an issue explaining what you want to do and how you're thinking doing it. Other users may have the same need and collaboration may lead to better results. The development workflow is the following: Fork the project and clone your fork Create a new branch to work on Run git submodule update --init --recursive Write the code that is needed :D Add integration tests if necessary Get the linters with make install_linters and install jq with the package manager of your OS Use make clean all to build image locally and run tests (note that tests work on Linux only ) Document your improvements if necessary (e.g. if you introduced new environment variables, describe those in the ENV documentation ) Commit and sign your commit , push and create a pull-request to merge into master . Please use the pull-request template to provide a minimum of contextual information and make sure to meet the requirements of the checklist. Pull requests are automatically tested against the CI and will be reviewed when tests pass When your changes are validated, your branch is merged CI builds the new :edge image immediately and your changes will be includes in the next version release.","title":"Issues and Pull Requests"},{"location":"contributing/issues-and-pull-requests/#opening-an-issue","text":"Attention Before opening an issue , read the README carefully, study the documentation , the Postfix/Dovecot documentation and your search engine you trust. The issue tracker is not meant to be used for unrelated questions! When opening an issue, please provide details use case to let the community reproduce your problem. Please start the mail server with env DMS_DEBUG=1 and paste the output into the issue. Attention Use the issue templates to provide the necessary information. Issues which do not use these templates are not worked on and closed. By raising issues, I agree to these terms and I understand, that the rules set for the issue tracker will help both maintainers as well as everyone to find a solution. Maintainers take the time to improve on this project and help by solving issues together. It is therefore expected from others to make an effort and comply with the rules .","title":"Opening an Issue"},{"location":"contributing/issues-and-pull-requests/#pull-requests","text":"","title":"Pull Requests"},{"location":"contributing/issues-and-pull-requests/#submit-a-pull-request","text":"Motivation You want to add a feature? Feel free to start creating an issue explaining what you want to do and how you're thinking doing it. Other users may have the same need and collaboration may lead to better results. The development workflow is the following: Fork the project and clone your fork Create a new branch to work on Run git submodule update --init --recursive Write the code that is needed :D Add integration tests if necessary Get the linters with make install_linters and install jq with the package manager of your OS Use make clean all to build image locally and run tests (note that tests work on Linux only ) Document your improvements if necessary (e.g. if you introduced new environment variables, describe those in the ENV documentation ) Commit and sign your commit , push and create a pull-request to merge into master . Please use the pull-request template to provide a minimum of contextual information and make sure to meet the requirements of the checklist. Pull requests are automatically tested against the CI and will be reviewed when tests pass When your changes are validated, your branch is merged CI builds the new :edge image immediately and your changes will be includes in the next version release.","title":"Submit a Pull-Request"},{"location":"contributing/tests/","text":"Todo This section should provide a detailed step by step guide on how to write tests","title":"Tests"},{"location":"examples/tutorials/basic-installation/","text":"Building a Simple Mailserver Warning Adding the docker network's gateway to the list of trusted hosts, e.g. using the network or connected-networks option, can create an open relay , for instance if IPv6 is enabled on the host machine but not in Docker . We are going to use this docker based mailserver: First create a directory for the mailserver and get the setup script: mkdir -p /var/ds/mail.example.org cd /var/ds/mail.example.org/ curl -o setup.sh \\ https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh chmod a+x ./setup.sh Create the file docker-compose.yml with a content like this: Example version : '2' services : mail : image : mailserver/docker-mailserver:latest hostname : mail domainname : example.org container_name : mail ports : - \"25:25\" - \"587:587\" - \"465:465\" volumes : - ./data/:/var/mail/ - ./state/:/var/mail-state/ - ./config/:/tmp/docker-mailserver/ - /var/ds/wsproxy/letsencrypt/:/etc/letsencrypt/ environment : - PERMIT_DOCKER=network - SSL_TYPE=letsencrypt - ONE_DIR=1 - DMS_DEBUG=1 - SPOOF_PROTECTION=0 - REPORT_RECIPIENT=1 - ENABLE_SPAMASSASSIN=0 - ENABLE_CLAMAV=0 - ENABLE_FAIL2BAN=1 - ENABLE_POSTGREY=0 cap_add : - NET_ADMIN - SYS_PTRACE For more details about the environment variables that can be used, and their meaning and possible values, check also these: Environment Variables mailserver.env file Make sure to set the proper domainname that you will use for the emails. We forward only SMTP ports (not POP3 and IMAP) because we are not interested in accessing the mailserver directly (from a client). We also use these settings: PERMIT_DOCKER=network because we want to send emails from other docker containers. SSL_TYPE=letsencrypt because we will manage SSL certificates with letsencrypt. We need to open ports 25 , 587 and 465 on the firewall: ufw allow 25 ufw allow 587 ufw allow 465 On your server you may have to do it differently. Pull the docker image: docker pull mailserver/docker-mailserver:latest Now generate the DKIM keys with ./setup.sh config dkim and copy the content of the file config/opendkim/keys/domain.tld/mail.txt on the domain zone configuration at the DNS server. I use bind9 for managing my domains, so I just paste it on example.org.db : mail._domainkey IN TXT ( \"v=DKIM1; h=sha256; k=rsa; \" \"p=MIIBIjANBgkqhkiG9w0BAQEFACAQ8AMIIBCgKCAQEAaH5KuPYPSF3Ppkt466BDMAFGOA4mgqn4oPjZ5BbFlYA9l5jU3bgzRj3l6/Q1n5a9lQs5fNZ7A/HtY0aMvs3nGE4oi+LTejt1jblMhV/OfJyRCunQBIGp0s8G9kIUBzyKJpDayk2+KJSJt/lxL9Iiy0DE5hIv62ZPP6AaTdHBAsJosLFeAzuLFHQ6USyQRojefqFQtgYqWQ2JiZQ3\" \"iqq3bD/BVlwKRp5gH6TEYEmx8EBJUuDxrJhkWRUk2VDl1fqhVBy8A9O7Ah+85nMrlOHIFsTaYo9o6+cDJ6t1i6G1gu+bZD0d3/3bqGLPBQV9LyEL1Rona5V7TJBGg099NQkTz1IwIDAQAB\" ) ; ----- DKIM key mail for example.org Add these configurations as well on the same file on the DNS server: mail IN A 10.11.12.13 ; mailservers for example.org 3600 IN MX 1 mail.example.org. ; Add SPF record IN TXT \"v=spf1 mx ~all\" Then don't forget to change the serial number and to restart the service. Get an SSL certificate from letsencrypt. I use wsproxy for managing SSL letsencrypt certificates of my domains: cd /var/ds/wsproxy ds domains-add mail mail.example.org ds get-ssl-cert myemail@gmail.com mail.example.org --test ds get-ssl-cert myemail@gmail.com mail.example.org Now the certificates will be available on /var/ds/wsproxy/letsencrypt/live/mail.example.org . Start the mailserver and check for any errors: apt install docker-compose docker-compose up mail Create email accounts and aliases with SPOOF_PROTECTION=0 : ./setup.sh email add admin@example.org passwd123 ./setup.sh email add info@example.org passwd123 ./setup.sh alias add admin@example.org myemail@gmail.com ./setup.sh alias add info@example.org myemail@gmail.com ./setup.sh email list ./setup.sh alias list Aliases make sure that any email that comes to these accounts is forwarded to my real email address, so that I don't need to use POP3/IMAP in order to get these messages. Also no anti-spam and anti-virus software is needed, making the mailserver lighter. Or create email accounts and aliases with SPOOF_PROTECTION=1 : ./setup.sh email add admin.gmail@example.org passwd123 ./setup.sh email add info.gmail@example.org passwd123 ./setup.sh alias add admin@example.org admin.gmail@example.org ./setup.sh alias add info@example.org info.gmail@example.org ./setup.sh alias add admin.gmail@example.org myemail@gmail.com ./setup.sh alias add info.gmail@example.org myemail@gmail.com ./setup.sh email list ./setup.sh alias list This extra step is required to avoid the 553 5.7.1 Sender address rejected: not owned by user error (the account used for setting up gmail is admin.gmail@example.org and info.gmail@example.org ) Send some test emails to these addresses and make other tests. Then stop the container with ctrl+c and start it again as a daemon: docker-compose up -d mail . Now save on Moodle configuration the SMTP settings and test by trying to send some messages to other users: SMTP hosts : mail.example.org:465 SMTP security : SSL SMTP username : info@example.org SMTP password : passwd123","title":"Basic Installation"},{"location":"examples/tutorials/basic-installation/#building-a-simple-mailserver","text":"Warning Adding the docker network's gateway to the list of trusted hosts, e.g. using the network or connected-networks option, can create an open relay , for instance if IPv6 is enabled on the host machine but not in Docker . We are going to use this docker based mailserver: First create a directory for the mailserver and get the setup script: mkdir -p /var/ds/mail.example.org cd /var/ds/mail.example.org/ curl -o setup.sh \\ https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh chmod a+x ./setup.sh Create the file docker-compose.yml with a content like this: Example version : '2' services : mail : image : mailserver/docker-mailserver:latest hostname : mail domainname : example.org container_name : mail ports : - \"25:25\" - \"587:587\" - \"465:465\" volumes : - ./data/:/var/mail/ - ./state/:/var/mail-state/ - ./config/:/tmp/docker-mailserver/ - /var/ds/wsproxy/letsencrypt/:/etc/letsencrypt/ environment : - PERMIT_DOCKER=network - SSL_TYPE=letsencrypt - ONE_DIR=1 - DMS_DEBUG=1 - SPOOF_PROTECTION=0 - REPORT_RECIPIENT=1 - ENABLE_SPAMASSASSIN=0 - ENABLE_CLAMAV=0 - ENABLE_FAIL2BAN=1 - ENABLE_POSTGREY=0 cap_add : - NET_ADMIN - SYS_PTRACE For more details about the environment variables that can be used, and their meaning and possible values, check also these: Environment Variables mailserver.env file Make sure to set the proper domainname that you will use for the emails. We forward only SMTP ports (not POP3 and IMAP) because we are not interested in accessing the mailserver directly (from a client). We also use these settings: PERMIT_DOCKER=network because we want to send emails from other docker containers. SSL_TYPE=letsencrypt because we will manage SSL certificates with letsencrypt. We need to open ports 25 , 587 and 465 on the firewall: ufw allow 25 ufw allow 587 ufw allow 465 On your server you may have to do it differently. Pull the docker image: docker pull mailserver/docker-mailserver:latest Now generate the DKIM keys with ./setup.sh config dkim and copy the content of the file config/opendkim/keys/domain.tld/mail.txt on the domain zone configuration at the DNS server. I use bind9 for managing my domains, so I just paste it on example.org.db : mail._domainkey IN TXT ( \"v=DKIM1; h=sha256; k=rsa; \" \"p=MIIBIjANBgkqhkiG9w0BAQEFACAQ8AMIIBCgKCAQEAaH5KuPYPSF3Ppkt466BDMAFGOA4mgqn4oPjZ5BbFlYA9l5jU3bgzRj3l6/Q1n5a9lQs5fNZ7A/HtY0aMvs3nGE4oi+LTejt1jblMhV/OfJyRCunQBIGp0s8G9kIUBzyKJpDayk2+KJSJt/lxL9Iiy0DE5hIv62ZPP6AaTdHBAsJosLFeAzuLFHQ6USyQRojefqFQtgYqWQ2JiZQ3\" \"iqq3bD/BVlwKRp5gH6TEYEmx8EBJUuDxrJhkWRUk2VDl1fqhVBy8A9O7Ah+85nMrlOHIFsTaYo9o6+cDJ6t1i6G1gu+bZD0d3/3bqGLPBQV9LyEL1Rona5V7TJBGg099NQkTz1IwIDAQAB\" ) ; ----- DKIM key mail for example.org Add these configurations as well on the same file on the DNS server: mail IN A 10.11.12.13 ; mailservers for example.org 3600 IN MX 1 mail.example.org. ; Add SPF record IN TXT \"v=spf1 mx ~all\" Then don't forget to change the serial number and to restart the service. Get an SSL certificate from letsencrypt. I use wsproxy for managing SSL letsencrypt certificates of my domains: cd /var/ds/wsproxy ds domains-add mail mail.example.org ds get-ssl-cert myemail@gmail.com mail.example.org --test ds get-ssl-cert myemail@gmail.com mail.example.org Now the certificates will be available on /var/ds/wsproxy/letsencrypt/live/mail.example.org . Start the mailserver and check for any errors: apt install docker-compose docker-compose up mail Create email accounts and aliases with SPOOF_PROTECTION=0 : ./setup.sh email add admin@example.org passwd123 ./setup.sh email add info@example.org passwd123 ./setup.sh alias add admin@example.org myemail@gmail.com ./setup.sh alias add info@example.org myemail@gmail.com ./setup.sh email list ./setup.sh alias list Aliases make sure that any email that comes to these accounts is forwarded to my real email address, so that I don't need to use POP3/IMAP in order to get these messages. Also no anti-spam and anti-virus software is needed, making the mailserver lighter. Or create email accounts and aliases with SPOOF_PROTECTION=1 : ./setup.sh email add admin.gmail@example.org passwd123 ./setup.sh email add info.gmail@example.org passwd123 ./setup.sh alias add admin@example.org admin.gmail@example.org ./setup.sh alias add info@example.org info.gmail@example.org ./setup.sh alias add admin.gmail@example.org myemail@gmail.com ./setup.sh alias add info.gmail@example.org myemail@gmail.com ./setup.sh email list ./setup.sh alias list This extra step is required to avoid the 553 5.7.1 Sender address rejected: not owned by user error (the account used for setting up gmail is admin.gmail@example.org and info.gmail@example.org ) Send some test emails to these addresses and make other tests. Then stop the container with ctrl+c and start it again as a daemon: docker-compose up -d mail . Now save on Moodle configuration the SMTP settings and test by trying to send some messages to other users: SMTP hosts : mail.example.org:465 SMTP security : SSL SMTP username : info@example.org SMTP password : passwd123","title":"Building a Simple Mailserver"},{"location":"examples/tutorials/mailserver-behind-proxy/","text":"Using docker-mailserver behind a Proxy Information If you are hiding your container behind a proxy service you might have discovered that the proxied requests from now on contain the proxy IP as the request origin. Whilst this behavior is technical correct it produces certain problems on the containers behind the proxy as they cannot distinguish the real origin of the requests anymore. To solve this problem on TCP connections we can make use of the proxy protocol . Compared to other workarounds that exist ( X-Forwarded-For which only works for HTTP requests or Tproxy that requires you to recompile your kernel) the proxy protocol: It is protocol agnostic (can work with any layer 7 protocols, even when encrypted). It does not require any infrastructure changes. NAT-ing firewalls have no impact it. It is scalable. There is only one condition: both endpoints of the connection MUST be compatible with proxy protocol. Luckily dovecot and postfix are both Proxy-Protocol ready softwares so it depends only on your used reverse-proxy / loadbalancer. Configuration of the used Proxy Software The configuration depends on the used proxy system. I will provide the configuration examples of traefik v2 using IMAP and SMTP with implicit TLS. Feel free to add your configuration if you achived the same goal using different proxy software below: Traefik v2 Truncated configuration of traefik itself: version : '3.7' services : reverse-proxy : image : traefik:v2.4 container_name : docker-traefik restart : always command : - \"--providers.docker\" - \"--providers.docker.exposedbydefault=false\" - \"--providers.docker.network=proxy\" - \"--entrypoints.web.address=:80\" - \"--entryPoints.websecure.address=:443\" - \"--entryPoints.smtp.address=:25\" - \"--entryPoints.smtp-ssl.address=:465\" - \"--entryPoints.imap-ssl.address=:993\" - \"--entryPoints.sieve.address=:4190\" ports : - \"25:25\" - \"465:465\" - \"993:993\" - \"4190:4190\" [ ... ] Truncated list of neccessary labels on the mailserver container: version : '2' services : mail : image : mailserver/docker-mailserver:release-v7.2.0 restart : always networks : - proxy labels : - \"traefik.enable=true\" - \"traefik.tcp.routers.smtp.rule=HostSNI(`*`)\" - \"traefik.tcp.routers.smtp.entrypoints=smtp\" - \"traefik.tcp.routers.smtp.service=smtp\" - \"traefik.tcp.services.smtp.loadbalancer.server.port=25\" - \"traefik.tcp.services.smtp.loadbalancer.proxyProtocol.version=1\" - \"traefik.tcp.routers.smtp-ssl.rule=HostSNI(`*`)\" - \"traefik.tcp.routers.smtp-ssl.entrypoints=smtp-ssl\" - \"traefik.tcp.routers.smtp-ssl.service=smtp-ssl\" - \"traefik.tcp.services.smtp-ssl.loadbalancer.server.port=465\" - \"traefik.tcp.services.smtp-ssl.loadbalancer.proxyProtocol.version=1\" - \"traefik.tcp.routers.imap-ssl.rule=HostSNI(`*`)\" - \"traefik.tcp.routers.imap-ssl.entrypoints=imap-ssl\" - \"traefik.tcp.routers.imap-ssl.service=imap-ssl\" - \"traefik.tcp.services.imap-ssl.loadbalancer.server.port=10993\" - \"traefik.tcp.services.imap-ssl.loadbalancer.proxyProtocol.version=2\" - \"traefik.tcp.routers.sieve.rule=HostSNI(`*`)\" - \"traefik.tcp.routers.sieve.entrypoints=sieve\" - \"traefik.tcp.routers.sieve.service=sieve\" - \"traefik.tcp.services.sieve.loadbalancer.server.port=4190\" [ ... ] Keep in mind that it is neccessary to use port 10993 here. More information below at dovecot configuration. Configuration of the Backend ( dovecot and postfix ) The following changes can be achived completely by adding the content to the appropriate files by using the projects function to overwrite config files . Changes for postfix can be applied by adding the following content to config/postfix-main.cf : postscreen_upstream_proxy_protocol = haproxy and to config/postfix-master.cf : submission/inet/smtpd_upstream_proxy_protocol = haproxy smtps/inet/smtpd_upstream_proxy_protocol = haproxy Changes for dovecot can be applied by adding the following content to config/dovecot.cf : haproxy_trusted_networks = , haproxy_timeout = 3 secs service imap-login { inet_listener imaps { haproxy = yes ssl = yes port = 10993 } } Note Port 10993 is used here to avoid conflicts with internal systems like postscreen and amavis as they will exchange messages on the default port and obviously have a different origin then compared to the proxy.","title":"Mailserver behind Proxy"},{"location":"examples/tutorials/mailserver-behind-proxy/#using-docker-mailserver-behind-a-proxy","text":"","title":"Using docker-mailserver behind a Proxy"},{"location":"examples/tutorials/mailserver-behind-proxy/#information","text":"If you are hiding your container behind a proxy service you might have discovered that the proxied requests from now on contain the proxy IP as the request origin. Whilst this behavior is technical correct it produces certain problems on the containers behind the proxy as they cannot distinguish the real origin of the requests anymore. To solve this problem on TCP connections we can make use of the proxy protocol . Compared to other workarounds that exist ( X-Forwarded-For which only works for HTTP requests or Tproxy that requires you to recompile your kernel) the proxy protocol: It is protocol agnostic (can work with any layer 7 protocols, even when encrypted). It does not require any infrastructure changes. NAT-ing firewalls have no impact it. It is scalable. There is only one condition: both endpoints of the connection MUST be compatible with proxy protocol. Luckily dovecot and postfix are both Proxy-Protocol ready softwares so it depends only on your used reverse-proxy / loadbalancer.","title":"Information"},{"location":"examples/tutorials/mailserver-behind-proxy/#configuration-of-the-used-proxy-software","text":"The configuration depends on the used proxy system. I will provide the configuration examples of traefik v2 using IMAP and SMTP with implicit TLS. Feel free to add your configuration if you achived the same goal using different proxy software below: Traefik v2 Truncated configuration of traefik itself: version : '3.7' services : reverse-proxy : image : traefik:v2.4 container_name : docker-traefik restart : always command : - \"--providers.docker\" - \"--providers.docker.exposedbydefault=false\" - \"--providers.docker.network=proxy\" - \"--entrypoints.web.address=:80\" - \"--entryPoints.websecure.address=:443\" - \"--entryPoints.smtp.address=:25\" - \"--entryPoints.smtp-ssl.address=:465\" - \"--entryPoints.imap-ssl.address=:993\" - \"--entryPoints.sieve.address=:4190\" ports : - \"25:25\" - \"465:465\" - \"993:993\" - \"4190:4190\" [ ... ] Truncated list of neccessary labels on the mailserver container: version : '2' services : mail : image : mailserver/docker-mailserver:release-v7.2.0 restart : always networks : - proxy labels : - \"traefik.enable=true\" - \"traefik.tcp.routers.smtp.rule=HostSNI(`*`)\" - \"traefik.tcp.routers.smtp.entrypoints=smtp\" - \"traefik.tcp.routers.smtp.service=smtp\" - \"traefik.tcp.services.smtp.loadbalancer.server.port=25\" - \"traefik.tcp.services.smtp.loadbalancer.proxyProtocol.version=1\" - \"traefik.tcp.routers.smtp-ssl.rule=HostSNI(`*`)\" - \"traefik.tcp.routers.smtp-ssl.entrypoints=smtp-ssl\" - \"traefik.tcp.routers.smtp-ssl.service=smtp-ssl\" - \"traefik.tcp.services.smtp-ssl.loadbalancer.server.port=465\" - \"traefik.tcp.services.smtp-ssl.loadbalancer.proxyProtocol.version=1\" - \"traefik.tcp.routers.imap-ssl.rule=HostSNI(`*`)\" - \"traefik.tcp.routers.imap-ssl.entrypoints=imap-ssl\" - \"traefik.tcp.routers.imap-ssl.service=imap-ssl\" - \"traefik.tcp.services.imap-ssl.loadbalancer.server.port=10993\" - \"traefik.tcp.services.imap-ssl.loadbalancer.proxyProtocol.version=2\" - \"traefik.tcp.routers.sieve.rule=HostSNI(`*`)\" - \"traefik.tcp.routers.sieve.entrypoints=sieve\" - \"traefik.tcp.routers.sieve.service=sieve\" - \"traefik.tcp.services.sieve.loadbalancer.server.port=4190\" [ ... ] Keep in mind that it is neccessary to use port 10993 here. More information below at dovecot configuration.","title":"Configuration of the used Proxy Software"},{"location":"examples/tutorials/mailserver-behind-proxy/#configuration-of-the-backend-dovecot-and-postfix","text":"The following changes can be achived completely by adding the content to the appropriate files by using the projects function to overwrite config files . Changes for postfix can be applied by adding the following content to config/postfix-main.cf : postscreen_upstream_proxy_protocol = haproxy and to config/postfix-master.cf : submission/inet/smtpd_upstream_proxy_protocol = haproxy smtps/inet/smtpd_upstream_proxy_protocol = haproxy Changes for dovecot can be applied by adding the following content to config/dovecot.cf : haproxy_trusted_networks = , haproxy_timeout = 3 secs service imap-login { inet_listener imaps { haproxy = yes ssl = yes port = 10993 } } Note Port 10993 is used here to avoid conflicts with internal systems like postscreen and amavis as they will exchange messages on the default port and obviously have a different origin then compared to the proxy.","title":"Configuration of the Backend (dovecot and postfix)"},{"location":"examples/uses-cases/forward-only-mailserver-with-ldap-authentication/","text":"Building a Forward-Only Mailserver A forward-only mailserver does not have any local mailboxes. Instead, it has only aliases that forward emails to external email accounts (for example to a gmail account). You can also send email from the localhost (the computer where the mailserver is installed), using as sender any of the alias addresses. The important settings for this setup (on mailserver.env ) are these: PERMIT_DOCKER = host ENABLE_POP3 = ENABLE_CLAMAV = 0 SMTP_ONLY = 1 ENABLE_SPAMASSASSIN = 0 ENABLE_FETCHMAIL = 0 Since there are no local mailboxes, we use SMTP_ONLY=1 to disable dovecot . We disable as well the other services that are related to local mailboxes ( POP3 , ClamAV , SpamAssassin , etc.) We can create aliases with ./setup.sh , like this: ./setup.sh alias add Authenticating with LDAP If you want to send emails from outside the mailserver you have to authenticate somehow (with a username and password). One way of doing it is described in this discussion . However if there are many user accounts, it is better to use authentication with LDAP. The settings for this on mailserver.env are: ENABLE_LDAP = 1 LDAP_START_TLS = yes LDAP_SERVER_HOST = ldap.example.org LDAP_SEARCH_BASE = ou=users,dc=example,dc=org LDAP_BIND_DN = cn=mailserver,dc=example,dc=org LDAP_BIND_PW = pass1234 ENABLE_SASLAUTHD = 1 SASLAUTHD_MECHANISMS = ldap SASLAUTHD_LDAP_SERVER = ldap.example.org SASLAUTHD_LDAP_START_TLS = yes SASLAUTHD_LDAP_BIND_DN = cn=mailserver,dc=example,dc=org SASLAUTHD_LDAP_PASSWORD = pass1234 SASLAUTHD_LDAP_SEARCH_BASE = ou=users,dc=example,dc=org SASLAUTHD_LDAP_FILTER = (&(uid=%U)(objectClass=inetOrgPerson)) My LDAP data structure is very basic, containing only the username, password, and the external email address where to forward emails for this user. An entry looks like this add uid = username,ou=users,dc=example,dc=org uid : username objectClass : inetOrgPerson sn : username cn : username userPassword : {SSHA}abcdefghi123456789 email : real-email-address@external-domain.com This structure is different from what is expected/assumed from the configuration scripts of the mailserver, so it doesn't work just by using the LDAP_QUERY_FILTER_... settings. Instead, I had to do custom configuration . I created the script config/user-patches.sh , with a content like this: #!/bin/bash rm -f /etc/postfix/ { ldap-groups.cf,ldap-domains.cf } postconf \\ \"virtual_mailbox_domains = /etc/postfix/vhost\" \\ \"virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf texthash:/etc/postfix/virtual\" \\ \"smtpd_sender_login_maps = ldap:/etc/postfix/ldap-users.cf\" sed -i /etc/postfix/ldap-users.cf \\ -e '/query_filter/d' \\ -e '/result_attribute/d' \\ -e '/result_format/d' cat <> /etc/postfix/ldap-users.cf query_filter = (uid=%u) result_attribute = uid result_format = %s@example.org EOF sed -i /etc/postfix/ldap-aliases.cf \\ -e '/domain/d' \\ -e '/query_filter/d' \\ -e '/result_attribute/d' cat <> /etc/postfix/ldap-aliases.cf domain = example.org query_filter = (uid=%u) result_attribute = mail EOF postfix reload You see that besides query_filter , I had to customize as well result_attribute and result_format . Sealso For more details about using LDAP see: LDAP managed mail server with Postfix and Dovecot for multiple domains Note Another solution that serves as a forward-only mailserver is this: https://gitlab.com/docker-scripts/postfix Tip One user reports only having success if ENABLE_LDAP=0 was set.","title":"Forward-Only Mailserver with LDAP"},{"location":"examples/uses-cases/forward-only-mailserver-with-ldap-authentication/#building-a-forward-only-mailserver","text":"A forward-only mailserver does not have any local mailboxes. Instead, it has only aliases that forward emails to external email accounts (for example to a gmail account). You can also send email from the localhost (the computer where the mailserver is installed), using as sender any of the alias addresses. The important settings for this setup (on mailserver.env ) are these: PERMIT_DOCKER = host ENABLE_POP3 = ENABLE_CLAMAV = 0 SMTP_ONLY = 1 ENABLE_SPAMASSASSIN = 0 ENABLE_FETCHMAIL = 0 Since there are no local mailboxes, we use SMTP_ONLY=1 to disable dovecot . We disable as well the other services that are related to local mailboxes ( POP3 , ClamAV , SpamAssassin , etc.) We can create aliases with ./setup.sh , like this: ./setup.sh alias add ","title":"Building a Forward-Only Mailserver"},{"location":"examples/uses-cases/forward-only-mailserver-with-ldap-authentication/#authenticating-with-ldap","text":"If you want to send emails from outside the mailserver you have to authenticate somehow (with a username and password). One way of doing it is described in this discussion . However if there are many user accounts, it is better to use authentication with LDAP. The settings for this on mailserver.env are: ENABLE_LDAP = 1 LDAP_START_TLS = yes LDAP_SERVER_HOST = ldap.example.org LDAP_SEARCH_BASE = ou=users,dc=example,dc=org LDAP_BIND_DN = cn=mailserver,dc=example,dc=org LDAP_BIND_PW = pass1234 ENABLE_SASLAUTHD = 1 SASLAUTHD_MECHANISMS = ldap SASLAUTHD_LDAP_SERVER = ldap.example.org SASLAUTHD_LDAP_START_TLS = yes SASLAUTHD_LDAP_BIND_DN = cn=mailserver,dc=example,dc=org SASLAUTHD_LDAP_PASSWORD = pass1234 SASLAUTHD_LDAP_SEARCH_BASE = ou=users,dc=example,dc=org SASLAUTHD_LDAP_FILTER = (&(uid=%U)(objectClass=inetOrgPerson)) My LDAP data structure is very basic, containing only the username, password, and the external email address where to forward emails for this user. An entry looks like this add uid = username,ou=users,dc=example,dc=org uid : username objectClass : inetOrgPerson sn : username cn : username userPassword : {SSHA}abcdefghi123456789 email : real-email-address@external-domain.com This structure is different from what is expected/assumed from the configuration scripts of the mailserver, so it doesn't work just by using the LDAP_QUERY_FILTER_... settings. Instead, I had to do custom configuration . I created the script config/user-patches.sh , with a content like this: #!/bin/bash rm -f /etc/postfix/ { ldap-groups.cf,ldap-domains.cf } postconf \\ \"virtual_mailbox_domains = /etc/postfix/vhost\" \\ \"virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf texthash:/etc/postfix/virtual\" \\ \"smtpd_sender_login_maps = ldap:/etc/postfix/ldap-users.cf\" sed -i /etc/postfix/ldap-users.cf \\ -e '/query_filter/d' \\ -e '/result_attribute/d' \\ -e '/result_format/d' cat <> /etc/postfix/ldap-users.cf query_filter = (uid=%u) result_attribute = uid result_format = %s@example.org EOF sed -i /etc/postfix/ldap-aliases.cf \\ -e '/domain/d' \\ -e '/query_filter/d' \\ -e '/result_attribute/d' cat <> /etc/postfix/ldap-aliases.cf domain = example.org query_filter = (uid=%u) result_attribute = mail EOF postfix reload You see that besides query_filter , I had to customize as well result_attribute and result_format . Sealso For more details about using LDAP see: LDAP managed mail server with Postfix and Dovecot for multiple domains Note Another solution that serves as a forward-only mailserver is this: https://gitlab.com/docker-scripts/postfix Tip One user reports only having success if ENABLE_LDAP=0 was set.","title":"Authenticating with LDAP"}]} \ No newline at end of file diff --git a/v10.0/sitemap.xml b/v10.0/sitemap.xml new file mode 100644 index 00000000..7a66dc18 --- /dev/null +++ b/v10.0/sitemap.xml @@ -0,0 +1,151 @@ + + + https://docker-mailserver.github.io/docker-mailserver/v10.0/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/introduction/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/setup.sh/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/environment/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/user-management/accounts/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/user-management/aliases/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/best-practices/dkim/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/best-practices/dmarc/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/best-practices/spf/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/best-practices/autodiscover/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/security/understanding-the-ports/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/security/ssl/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/security/fail2ban/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/security/mail_crypt/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/troubleshooting/debugging/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/pop3/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/optional-config/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/maintenance/update-and-cleanup/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/override-defaults/dovecot/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/override-defaults/postfix/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/override-defaults/user-patches/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/auth-ldap/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/mail-sieve/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/mail-fetchmail/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/mail-forwarding/relay-hosts/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/mail-forwarding/aws-ses/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/full-text-search/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/kubernetes/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/config/advanced/ipv6/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/examples/tutorials/basic-installation/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/examples/tutorials/mailserver-behind-proxy/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/examples/uses-cases/forward-only-mailserver-with-ldap-authentication/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/faq/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/contributing/issues-and-pull-requests/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/contributing/coding-style/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/contributing/tests/ + 2021-06-01 + daily + + https://docker-mailserver.github.io/docker-mailserver/v10.0/contributing/documentation/ + 2021-06-01 + daily + + \ No newline at end of file