From ffb9043ba5c430695f1a1e10da743a9a75683401 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Dec 2023 12:59:39 +0000 Subject: [PATCH] deploy: 8392e3c1a8db660e6fd458b1bf426ab4736104c4 --- v13.1/assets/css/customizations.css | 100 + v13.1/assets/fonts/external-link.woff | Bin 0 -> 1008 bytes v13.1/assets/img/bg-water.webp | Bin 0 -> 92840 bytes .../assets/javascripts/bundle.dff1b7c8.min.js | 29 + .../workers/search.dfff1995.min.js | 42 + v13.1/assets/logo/dmo-logo-white.min.svg | Bin 0 -> 677 bytes v13.1/assets/logo/dmo-logo.min.svg | Bin 0 -> 1084 bytes v13.1/assets/logo/favicon-32x32.png | Bin 0 -> 1130 bytes .../assets/stylesheets/main.046329b4.min.css | 1 + .../stylesheets/palette.85d0ee34.min.css | 1 + v13.1/config/advanced/auth-ldap/index.html | 2380 ++++++++ .../dovecot-master-accounts/index.html | 2026 +++++++ .../advanced/full-text-search/index.html | 2254 ++++++++ v13.1/config/advanced/ipv6/index.html | 2253 ++++++++ v13.1/config/advanced/kubernetes/index.html | 2672 +++++++++ .../config/advanced/mail-fetchmail/index.html | 2148 ++++++++ .../mail-forwarding/aws-ses/index.html | 1951 +++++++ .../mail-forwarding/relay-hosts/index.html | 2124 ++++++++ v13.1/config/advanced/mail-getmail/index.html | 2113 +++++++ v13.1/config/advanced/mail-sieve/index.html | 2097 +++++++ .../maintenance/update-and-cleanup/index.html | 1971 +++++++ .../advanced/optional-config/index.html | 2046 +++++++ .../override-defaults/dovecot/index.html | 2058 +++++++ .../override-defaults/postfix/index.html | 1958 +++++++ .../override-defaults/user-patches/index.html | 1963 +++++++ v13.1/config/advanced/podman/index.html | 2252 ++++++++ .../best-practices/autodiscover/index.html | 1951 +++++++ .../best-practices/dkim_dmarc_spf/index.html | 2397 ++++++++ v13.1/config/debugging/index.html | 2268 ++++++++ v13.1/config/environment/index.html | 4845 +++++++++++++++++ v13.1/config/pop3/index.html | 1953 +++++++ v13.1/config/security/fail2ban/index.html | 2178 ++++++++ v13.1/config/security/mail_crypt/index.html | 2047 +++++++ v13.1/config/security/rspamd/index.html | 2437 +++++++++ v13.1/config/security/ssl/index.html | 3007 ++++++++++ .../understanding-the-ports/index.html | 2322 ++++++++ v13.1/config/setup.sh/index.html | 1955 +++++++ v13.1/config/user-management/index.html | 2197 ++++++++ v13.1/contributing/general/index.html | 2017 +++++++ .../issues-and-pull-requests/index.html | 2088 +++++++ v13.1/contributing/tests/index.html | 2230 ++++++++ .../tutorials/basic-installation/index.html | 2214 ++++++++ .../examples/tutorials/blog-posts/index.html | 1942 +++++++ v13.1/examples/tutorials/crowdsec/index.html | 2099 +++++++ .../tutorials/docker-build/index.html | 2101 +++++++ .../mailserver-behind-proxy/index.html | 2145 ++++++++ v13.1/examples/use-cases/auth-lua/index.html | 2166 ++++++++ .../index.html | 2087 +++++++ .../use-cases/imap-folders/index.html | 2128 ++++++++ .../ios-mail-push-support/index.html | 2283 ++++++++ v13.1/faq/index.html | 2950 ++++++++++ v13.1/favicon.ico | Bin 0 -> 15086 bytes v13.1/index.html | 2105 +++++++ v13.1/introduction/index.html | 2308 ++++++++ v13.1/search/search_index.json | 1 + v13.1/sitemap.xml | 223 + v13.1/usage/index.html | 2354 ++++++++ 57 files changed, 99437 insertions(+) create mode 100644 v13.1/assets/css/customizations.css create mode 100644 v13.1/assets/fonts/external-link.woff create mode 100644 v13.1/assets/img/bg-water.webp create mode 100644 v13.1/assets/javascripts/bundle.dff1b7c8.min.js create mode 100644 v13.1/assets/javascripts/workers/search.dfff1995.min.js create mode 100644 v13.1/assets/logo/dmo-logo-white.min.svg create mode 100644 v13.1/assets/logo/dmo-logo.min.svg create mode 100644 v13.1/assets/logo/favicon-32x32.png create mode 100644 v13.1/assets/stylesheets/main.046329b4.min.css create mode 100644 v13.1/assets/stylesheets/palette.85d0ee34.min.css create mode 100644 v13.1/config/advanced/auth-ldap/index.html create mode 100644 v13.1/config/advanced/dovecot-master-accounts/index.html create mode 100644 v13.1/config/advanced/full-text-search/index.html create mode 100644 v13.1/config/advanced/ipv6/index.html create mode 100644 v13.1/config/advanced/kubernetes/index.html create mode 100644 v13.1/config/advanced/mail-fetchmail/index.html create mode 100644 v13.1/config/advanced/mail-forwarding/aws-ses/index.html create mode 100644 v13.1/config/advanced/mail-forwarding/relay-hosts/index.html create mode 100644 v13.1/config/advanced/mail-getmail/index.html create mode 100644 v13.1/config/advanced/mail-sieve/index.html create mode 100644 v13.1/config/advanced/maintenance/update-and-cleanup/index.html create mode 100644 v13.1/config/advanced/optional-config/index.html create mode 100644 v13.1/config/advanced/override-defaults/dovecot/index.html create mode 100644 v13.1/config/advanced/override-defaults/postfix/index.html create mode 100644 v13.1/config/advanced/override-defaults/user-patches/index.html create mode 100644 v13.1/config/advanced/podman/index.html create mode 100644 v13.1/config/best-practices/autodiscover/index.html create mode 100644 v13.1/config/best-practices/dkim_dmarc_spf/index.html create mode 100644 v13.1/config/debugging/index.html create mode 100644 v13.1/config/environment/index.html create mode 100644 v13.1/config/pop3/index.html create mode 100644 v13.1/config/security/fail2ban/index.html create mode 100644 v13.1/config/security/mail_crypt/index.html create mode 100644 v13.1/config/security/rspamd/index.html create mode 100644 v13.1/config/security/ssl/index.html create mode 100644 v13.1/config/security/understanding-the-ports/index.html create mode 100644 v13.1/config/setup.sh/index.html create mode 100644 v13.1/config/user-management/index.html create mode 100644 v13.1/contributing/general/index.html create mode 100644 v13.1/contributing/issues-and-pull-requests/index.html create mode 100644 v13.1/contributing/tests/index.html create mode 100644 v13.1/examples/tutorials/basic-installation/index.html create mode 100644 v13.1/examples/tutorials/blog-posts/index.html create mode 100644 v13.1/examples/tutorials/crowdsec/index.html create mode 100644 v13.1/examples/tutorials/docker-build/index.html create mode 100644 v13.1/examples/tutorials/mailserver-behind-proxy/index.html create mode 100644 v13.1/examples/use-cases/auth-lua/index.html create mode 100644 v13.1/examples/use-cases/forward-only-mailserver-with-ldap-authentication/index.html create mode 100644 v13.1/examples/use-cases/imap-folders/index.html create mode 100644 v13.1/examples/use-cases/ios-mail-push-support/index.html create mode 100644 v13.1/faq/index.html create mode 100644 v13.1/favicon.ico create mode 100644 v13.1/index.html create mode 100644 v13.1/introduction/index.html create mode 100644 v13.1/search/search_index.json create mode 100644 v13.1/sitemap.xml create mode 100644 v13.1/usage/index.html diff --git a/v13.1/assets/css/customizations.css b/v13.1/assets/css/customizations.css new file mode 100644 index 00000000..49ede009 --- /dev/null +++ b/v13.1/assets/css/customizations.css @@ -0,0 +1,100 @@ +/* + This file adds our styling additions / fixes to maintain. + Some of which are overly specific and may break with future updates by upstream. +*/ + +/* ============================================================================================================= */ + +/* 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 left navbar visibility on top-level (tabbed) pages with no nested sub-pages. + Upstream will not fix: https://github.com/squidfunk/mkdocs-material/issues/3109 +*/ + +@media screen and (min-width: 76.25em) { + .md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link { + display: none; + } +} + +/* ============================================================================================================= */ + +/* + 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/v13.1/assets/fonts/external-link.woff b/v13.1/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/v13.1/assets/javascripts/bundle.dff1b7c8.min.js b/v13.1/assets/javascripts/bundle.dff1b7c8.min.js new file mode 100644 index 00000000..a89e799a --- /dev/null +++ b/v13.1/assets/javascripts/bundle.dff1b7c8.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var gi=Object.create;var dr=Object.defineProperty;var xi=Object.getOwnPropertyDescriptor;var yi=Object.getOwnPropertyNames,Ht=Object.getOwnPropertySymbols,Ei=Object.getPrototypeOf,hr=Object.prototype.hasOwnProperty,Xr=Object.prototype.propertyIsEnumerable;var Jr=(e,t,r)=>t in e?dr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,I=(e,t)=>{for(var r in t||(t={}))hr.call(t,r)&&Jr(e,r,t[r]);if(Ht)for(var r of Ht(t))Xr.call(t,r)&&Jr(e,r,t[r]);return e};var Zr=(e,t)=>{var r={};for(var o in e)hr.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&&Xr.call(e,o)&&(r[o]=e[o]);return r};var br=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var wi=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of yi(t))!hr.call(e,n)&&n!==r&&dr(e,n,{get:()=>t[n],enumerable:!(o=xi(t,n))||o.enumerable});return e};var $t=(e,t,r)=>(r=e!=null?gi(Ei(e)):{},wi(t||!e||!e.__esModule?dr(r,"default",{value:e,enumerable:!0}):r,e));var to=br((vr,eo)=>{(function(e,t){typeof vr=="object"&&typeof eo!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(vr,function(){"use strict";function e(r){var o=!0,n=!1,i=null,s={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 a(A){return!!(A&&A!==document&&A.nodeName!=="HTML"&&A.nodeName!=="BODY"&&"classList"in A&&"contains"in A.classList)}function c(A){var it=A.type,Ne=A.tagName;return!!(Ne==="INPUT"&&s[it]&&!A.readOnly||Ne==="TEXTAREA"&&!A.readOnly||A.isContentEditable)}function p(A){A.classList.contains("focus-visible")||(A.classList.add("focus-visible"),A.setAttribute("data-focus-visible-added",""))}function m(A){A.hasAttribute("data-focus-visible-added")&&(A.classList.remove("focus-visible"),A.removeAttribute("data-focus-visible-added"))}function f(A){A.metaKey||A.altKey||A.ctrlKey||(a(r.activeElement)&&p(r.activeElement),o=!0)}function u(A){o=!1}function d(A){a(A.target)&&(o||c(A.target))&&p(A.target)}function b(A){a(A.target)&&(A.target.classList.contains("focus-visible")||A.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),m(A.target))}function _(A){document.visibilityState==="hidden"&&(n&&(o=!0),re())}function re(){document.addEventListener("mousemove",Y),document.addEventListener("mousedown",Y),document.addEventListener("mouseup",Y),document.addEventListener("pointermove",Y),document.addEventListener("pointerdown",Y),document.addEventListener("pointerup",Y),document.addEventListener("touchmove",Y),document.addEventListener("touchstart",Y),document.addEventListener("touchend",Y)}function Z(){document.removeEventListener("mousemove",Y),document.removeEventListener("mousedown",Y),document.removeEventListener("mouseup",Y),document.removeEventListener("pointermove",Y),document.removeEventListener("pointerdown",Y),document.removeEventListener("pointerup",Y),document.removeEventListener("touchmove",Y),document.removeEventListener("touchstart",Y),document.removeEventListener("touchend",Y)}function Y(A){A.target.nodeName&&A.target.nodeName.toLowerCase()==="html"||(o=!1,Z())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",_,!0),re(),r.addEventListener("focus",d,!0),r.addEventListener("blur",b,!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 Vr=br((Mt,Dr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Mt=="object"&&typeof Dr=="object"?Dr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Mt=="object"?Mt.ClipboardJS=r():t.ClipboardJS=r()})(Mt,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return vi}});var s=i(279),a=i.n(s),c=i(370),p=i.n(c),m=i(817),f=i.n(m);function u(F){try{return document.execCommand(F)}catch(S){return!1}}var d=function(S){var y=f()(S);return u("cut"),y},b=d;function _(F){var S=document.documentElement.getAttribute("dir")==="rtl",y=document.createElement("textarea");y.style.fontSize="12pt",y.style.border="0",y.style.padding="0",y.style.margin="0",y.style.position="absolute",y.style[S?"right":"left"]="-9999px";var R=window.pageYOffset||document.documentElement.scrollTop;return y.style.top="".concat(R,"px"),y.setAttribute("readonly",""),y.value=F,y}var re=function(S,y){var R=_(S);y.container.appendChild(R);var P=f()(R);return u("copy"),R.remove(),P},Z=function(S){var y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},R="";return typeof S=="string"?R=re(S,y):S instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(S==null?void 0:S.type)?R=re(S.value,y):(R=f()(S),u("copy")),R},Y=Z;function A(F){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?A=function(y){return typeof y}:A=function(y){return y&&typeof Symbol=="function"&&y.constructor===Symbol&&y!==Symbol.prototype?"symbol":typeof y},A(F)}var it=function(){var S=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},y=S.action,R=y===void 0?"copy":y,P=S.container,q=S.target,Me=S.text;if(R!=="copy"&&R!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&A(q)==="object"&&q.nodeType===1){if(R==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(R==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Me)return Y(Me,{container:P});if(q)return R==="cut"?b(q):Y(q,{container:P})},Ne=it;function Ie(F){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(y){return typeof y}:Ie=function(y){return y&&typeof Symbol=="function"&&y.constructor===Symbol&&y!==Symbol.prototype?"symbol":typeof y},Ie(F)}function pi(F,S){if(!(F instanceof S))throw new TypeError("Cannot call a class as a function")}function Gr(F,S){for(var y=0;y0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof P.action=="function"?P.action:this.defaultAction,this.target=typeof P.target=="function"?P.target:this.defaultTarget,this.text=typeof P.text=="function"?P.text:this.defaultText,this.container=Ie(P.container)==="object"?P.container:document.body}},{key:"listenClick",value:function(P){var q=this;this.listener=p()(P,"click",function(Me){return q.onClick(Me)})}},{key:"onClick",value:function(P){var q=P.delegateTarget||P.currentTarget,Me=this.action(q)||"copy",kt=Ne({action:Me,container:this.container,target:this.target(q),text:this.text(q)});this.emit(kt?"success":"error",{action:Me,text:kt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(P){return ur("action",P)}},{key:"defaultTarget",value:function(P){var q=ur("target",P);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(P){return ur("text",P)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(P){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return Y(P,q)}},{key:"cut",value:function(P){return b(P)}},{key:"isSupported",value:function(){var P=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof P=="string"?[P]:P,Me=!!document.queryCommandSupported;return q.forEach(function(kt){Me=Me&&!!document.queryCommandSupported(kt)}),Me}}]),y}(a()),vi=bi},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 s(a,c){for(;a&&a.nodeType!==n;){if(typeof a.matches=="function"&&a.matches(c))return a;a=a.parentNode}}o.exports=s},438:function(o,n,i){var s=i(828);function a(m,f,u,d,b){var _=p.apply(this,arguments);return m.addEventListener(u,_,b),{destroy:function(){m.removeEventListener(u,_,b)}}}function c(m,f,u,d,b){return typeof m.addEventListener=="function"?a.apply(null,arguments):typeof u=="function"?a.bind(null,document).apply(null,arguments):(typeof m=="string"&&(m=document.querySelectorAll(m)),Array.prototype.map.call(m,function(_){return a(_,f,u,d,b)}))}function p(m,f,u,d){return function(b){b.delegateTarget=s(b.target,f),b.delegateTarget&&d.call(m,b)}}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 s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[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 s=Object.prototype.toString.call(i);return s==="[object Function]"}},370:function(o,n,i){var s=i(879),a=i(438);function c(u,d,b){if(!u&&!d&&!b)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(b))throw new TypeError("Third argument must be a Function");if(s.node(u))return p(u,d,b);if(s.nodeList(u))return m(u,d,b);if(s.string(u))return f(u,d,b);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function p(u,d,b){return u.addEventListener(d,b),{destroy:function(){u.removeEventListener(d,b)}}}function m(u,d,b){return Array.prototype.forEach.call(u,function(_){_.addEventListener(d,b)}),{destroy:function(){Array.prototype.forEach.call(u,function(_){_.removeEventListener(d,b)})}}}function f(u,d,b){return a(document.body,u,d,b)}o.exports=c},817:function(o){function n(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),p=document.createRange();p.selectNodeContents(i),c.removeAllRanges(),c.addRange(p),s=c.toString()}return s}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,s,a){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var c=this;function p(){c.off(i,p),s.apply(a,arguments)}return p._=s,this.on(i,p,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),c=0,p=a.length;for(c;c{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var _a=/["'&<>]/;Pn.exports=Aa;function Aa(e){var t=""+e,r=_a.exec(t);if(!r)return t;var o,n="",i=0,s=0;for(i=r.index;i0&&i[i.length-1])&&(p[0]===6||p[0]===2)){r=0;continue}if(p[0]===3&&(!i||p[1]>i[0]&&p[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function U(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],s;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(a){s={error:a}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(s)throw s.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||a(u,d)})})}function a(u,d){try{c(o[u](d))}catch(b){f(i[0][3],b)}}function c(u){u.value instanceof Ze?Promise.resolve(u.value.v).then(p,m):f(i[0][2],u)}function p(u){a("next",u)}function m(u){a("throw",u)}function f(u,d){u(d),i.shift(),i.length&&a(i[0][0],i[0][1])}}function no(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Ee=="function"?Ee(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(s){return new Promise(function(a,c){s=e[i](s),n(a,c,s.done,s.value)})}}function n(i,s,a,c){Promise.resolve(c).then(function(p){i({value:p,done:a})},s)}}function C(e){return typeof e=="function"}function at(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var It=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function De(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Pe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Ee(s),c=a.next();!c.done;c=a.next()){var p=c.value;p.remove(this)}}catch(_){t={error:_}}finally{try{c&&!c.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var m=this.initialTeardown;if(C(m))try{m()}catch(_){i=_ instanceof It?_.errors:[_]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=Ee(f),d=u.next();!d.done;d=u.next()){var b=d.value;try{io(b)}catch(_){i=i!=null?i:[],_ instanceof It?i=D(D([],U(i)),U(_.errors)):i.push(_)}}}catch(_){o={error:_}}finally{try{d&&!d.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new It(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)io(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&De(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&De(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var xr=Pe.EMPTY;function Pt(e){return e instanceof Pe||e&&"closed"in e&&C(e.remove)&&C(e.add)&&C(e.unsubscribe)}function io(e){C(e)?e():e.unsubscribe()}var Le={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],o=2;o0},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=this,i=n.hasError,s=n.isStopped,a=n.observers;return i||s?xr:(this.currentObservers=null,a.push(r),new Pe(function(){o.currentObservers=null,De(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,s=o.isStopped;n?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new j;return r.source=this,r},t.create=function(r,o){return new uo(r,o)},t}(j);var uo=function(e){ie(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:xr},t}(x);var yt={now:function(){return(yt.delegate||Date).now()},delegate:void 0};var Et=function(e){ie(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=yt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,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,s=o._infiniteTimeWindow,a=o._timestampProvider,c=o._windowTime;n||(i.push(r),!s&&i.push(a.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,s=n._buffer,a=s.slice(),c=0;c0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=mt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var s=r.actions;o!=null&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==o&&(mt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(Wt);var vo=function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(Ut);var Te=new vo(bo);var T=new j(function(e){return e.complete()});function Nt(e){return e&&C(e.schedule)}function Mr(e){return e[e.length-1]}function Qe(e){return C(Mr(e))?e.pop():void 0}function Oe(e){return Nt(Mr(e))?e.pop():void 0}function Dt(e,t){return typeof Mr(e)=="number"?e.pop():t}var lt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Vt(e){return C(e==null?void 0:e.then)}function zt(e){return C(e[pt])}function qt(e){return Symbol.asyncIterator&&C(e==null?void 0:e[Symbol.asyncIterator])}function Kt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function ki(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qt=ki();function Yt(e){return C(e==null?void 0:e[Qt])}function Bt(e){return oo(this,arguments,function(){var r,o,n,i;return Rt(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,Ze(r.read())];case 3:return o=s.sent(),n=o.value,i=o.done,i?[4,Ze(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,Ze(n)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Gt(e){return C(e==null?void 0:e.getReader)}function W(e){if(e instanceof j)return e;if(e!=null){if(zt(e))return Hi(e);if(lt(e))return $i(e);if(Vt(e))return Ri(e);if(qt(e))return go(e);if(Yt(e))return Ii(e);if(Gt(e))return Pi(e)}throw Kt(e)}function Hi(e){return new j(function(t){var r=e[pt]();if(C(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function $i(e){return new j(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?L(function(n,i){return e(n,i,o)}):de,ge(1),r?He(t):Io(function(){return new Xt}))}}function Po(){for(var e=[],t=0;t=2,!0))}function le(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new x}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,c=a===void 0?!0:a;return function(p){var m,f,u,d=0,b=!1,_=!1,re=function(){f==null||f.unsubscribe(),f=void 0},Z=function(){re(),m=u=void 0,b=_=!1},Y=function(){var A=m;Z(),A==null||A.unsubscribe()};return g(function(A,it){d++,!_&&!b&&re();var Ne=u=u!=null?u:r();it.add(function(){d--,d===0&&!_&&!b&&(f=kr(Y,c))}),Ne.subscribe(it),!m&&d>0&&(m=new tt({next:function(Ie){return Ne.next(Ie)},error:function(Ie){_=!0,re(),f=kr(Z,n,Ie),Ne.error(Ie)},complete:function(){b=!0,re(),f=kr(Z,s),Ne.complete()}}),W(A).subscribe(m))})(p)}}function kr(e,t){for(var r=[],o=2;oe.next(document)),e}function z(e,t=document){return Array.from(t.querySelectorAll(e))}function N(e,t=document){let r=ce(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ce(e,t=document){return t.querySelector(e)||void 0}function Re(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}var ea=M(h(document.body,"focusin"),h(document.body,"focusout")).pipe(ke(1),V(void 0),l(()=>Re()||document.body),B(1));function er(e){return ea.pipe(l(t=>e.contains(t)),G())}function Je(e){return{x:e.offsetLeft,y:e.offsetTop}}function Uo(e){return M(h(window,"load"),h(window,"resize")).pipe(Ae(0,Te),l(()=>Je(e)),V(Je(e)))}function tr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return M(h(e,"scroll"),h(window,"resize")).pipe(Ae(0,Te),l(()=>tr(e)),V(tr(e)))}function No(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)No(e,r)}function O(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)No(o,n);return o}function rr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ht(e){let t=O("script",{src:e});return $(()=>(document.head.appendChild(t),M(h(t,"load"),h(t,"error").pipe(v(()=>St(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),k(()=>document.head.removeChild(t)),ge(1))))}var Do=new x,ta=$(()=>typeof ResizeObserver=="undefined"?ht("https://unpkg.com/resize-observer-polyfill"):H(void 0)).pipe(l(()=>new ResizeObserver(e=>{for(let t of e)Do.next(t)})),v(e=>M(Ve,H(e)).pipe(k(()=>e.disconnect()))),B(1));function he(e){return{width:e.offsetWidth,height:e.offsetHeight}}function xe(e){return ta.pipe(w(t=>t.observe(e)),v(t=>Do.pipe(L(({target:r})=>r===e),k(()=>t.unobserve(e)),l(()=>he(e)))),V(he(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function or(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var Vo=new x,ra=$(()=>H(new IntersectionObserver(e=>{for(let t of e)Vo.next(t)},{threshold:0}))).pipe(v(e=>M(Ve,H(e)).pipe(k(()=>e.disconnect()))),B(1));function nr(e){return ra.pipe(w(t=>t.observe(e)),v(t=>Vo.pipe(L(({target:r})=>r===e),k(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function zo(e,t=16){return dt(e).pipe(l(({y:r})=>{let o=he(e),n=bt(e);return r>=n.height-o.height-t}),G())}var ir={drawer:N("[data-md-toggle=drawer]"),search:N("[data-md-toggle=search]")};function qo(e){return ir[e].checked}function Ke(e,t){ir[e].checked!==t&&ir[e].click()}function We(e){let t=ir[e];return h(t,"change").pipe(l(()=>t.checked),V(t.checked))}function oa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function na(){return M(h(window,"compositionstart").pipe(l(()=>!0)),h(window,"compositionend").pipe(l(()=>!1))).pipe(V(!1))}function Ko(){let e=h(window,"keydown").pipe(L(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:qo("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let o=Re();if(typeof o!="undefined")return!oa(o,r)}return!0}),le());return na().pipe(v(t=>t?T:e))}function fe(){return new URL(location.href)}function ot(e){location.href=e.href}function Qo(){return new x}function Yo(){return location.hash.slice(1)}function Pr(e){let t=O("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function ia(e){return M(h(window,"hashchange"),e).pipe(l(Yo),V(Yo()),L(t=>t.length>0),B(1))}function Bo(e){return ia(e).pipe(l(t=>ce(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Fr(e){let t=matchMedia(e);return Zt(r=>t.addListener(()=>r(t.matches))).pipe(V(t.matches))}function Go(){let e=matchMedia("print");return M(h(window,"beforeprint").pipe(l(()=>!0)),h(window,"afterprint").pipe(l(()=>!1))).pipe(V(e.matches))}function jr(e,t){return e.pipe(v(r=>r?t():T))}function ar(e,t={credentials:"same-origin"}){return me(fetch(`${e}`,t)).pipe(pe(()=>T),v(r=>r.status!==200?St(()=>new Error(r.statusText)):H(r)))}function Ue(e,t){return ar(e,t).pipe(v(r=>r.json()),B(1))}function Jo(e,t){let r=new DOMParser;return ar(e,t).pipe(v(o=>o.text()),l(o=>r.parseFromString(o,"text/xml")),B(1))}function Xo(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function Zo(){return M(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(l(Xo),V(Xo()))}function en(){return{width:innerWidth,height:innerHeight}}function tn(){return h(window,"resize",{passive:!0}).pipe(l(en),V(en()))}function rn(){return Q([Zo(),tn()]).pipe(l(([e,t])=>({offset:e,size:t})),B(1))}function sr(e,{viewport$:t,header$:r}){let o=t.pipe(X("size")),n=Q([o,r]).pipe(l(()=>Je(e)));return Q([r,t,n]).pipe(l(([{height:i},{offset:s,size:a},{x:c,y:p}])=>({offset:{x:s.x-c,y:s.y-p+i},size:a})))}function aa(e){return h(e,"message",t=>t.data)}function sa(e){let t=new x;return t.subscribe(r=>e.postMessage(r)),t}function on(e,t=new Worker(e)){let r=aa(t),o=sa(t),n=new x;n.subscribe(o);let i=o.pipe(J(),ee(!0));return n.pipe(J(),qe(r.pipe(K(i))),le())}var ca=N("#__config"),vt=JSON.parse(ca.textContent);vt.base=`${new URL(vt.base,fe())}`;function ue(){return vt}function te(e){return vt.features.includes(e)}function be(e,t){return typeof t!="undefined"?vt.translations[e].replace("#",t.toString()):vt.translations[e]}function ye(e,t=document){return N(`[data-md-component=${e}]`,t)}function ne(e,t=document){return z(`[data-md-component=${e}]`,t)}function pa(e){let t=N(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(l(()=>N(".md-typeset",e)),l(r=>({hash:__md_hash(r.innerHTML)})))}function nn(e){if(!te("announce.dismiss")||!e.childElementCount)return T;if(!e.hidden){let t=N(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return $(()=>{let t=new x;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),pa(e).pipe(w(r=>t.next(r)),k(()=>t.complete()),l(r=>I({ref:e},r)))})}function ma(e,{target$:t}){return t.pipe(l(r=>({hidden:r!==e})))}function an(e,t){let r=new x;return r.subscribe(({hidden:o})=>{e.hidden=o}),ma(e,t).pipe(w(o=>r.next(o)),k(()=>r.complete()),l(o=>I({ref:e},o)))}function la(e,t){let r=$(()=>Q([Uo(e),dt(t)])).pipe(l(([{x:o,y:n},i])=>{let{width:s,height:a}=he(e);return{x:o-i.x+s/2,y:n-i.y+a/2}}));return er(e).pipe(v(o=>r.pipe(l(n=>({active:o,offset:n})),ge(+!o||1/0))))}function sn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return $(()=>{let i=new x,s=i.pipe(J(),ee(!0));return i.subscribe({next({offset:a}){e.style.setProperty("--md-tooltip-x",`${a.x}px`),e.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),nr(e).pipe(K(s)).subscribe(a=>{e.toggleAttribute("data-md-visible",a)}),M(i.pipe(L(({active:a})=>a)),i.pipe(ke(250),L(({active:a})=>!a))).subscribe({next({active:a}){a?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Ae(16,Te)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(Rr(125,Te),L(()=>!!e.offsetParent),l(()=>e.offsetParent.getBoundingClientRect()),l(({x:a})=>a)).subscribe({next(a){a?e.style.setProperty("--md-tooltip-0",`${-a}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),h(n,"click").pipe(K(s),L(a=>!(a.metaKey||a.ctrlKey))).subscribe(a=>{a.stopPropagation(),a.preventDefault()}),h(n,"mousedown").pipe(K(s),oe(i)).subscribe(([a,{active:c}])=>{var p;if(a.button!==0||a.metaKey||a.ctrlKey)a.preventDefault();else if(c){a.preventDefault();let m=e.parentElement.closest(".md-annotation");m instanceof HTMLElement?m.focus():(p=Re())==null||p.blur()}}),r.pipe(K(s),L(a=>a===o),ze(125)).subscribe(()=>e.focus()),la(e,t).pipe(w(a=>i.next(a)),k(()=>i.complete()),l(a=>I({ref:e},a)))})}function Wr(e){return O("div",{class:"md-tooltip",id:e},O("div",{class:"md-tooltip__inner md-typeset"}))}function cn(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return O("aside",{class:"md-annotation",tabIndex:0},Wr(t),O("a",{href:r,class:"md-annotation__index",tabIndex:-1},O("span",{"data-md-annotation-id":e})))}else return O("aside",{class:"md-annotation",tabIndex:0},Wr(t),O("span",{class:"md-annotation__index",tabIndex:-1},O("span",{"data-md-annotation-id":e})))}function pn(e){return O("button",{class:"md-clipboard md-icon",title:be("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function Ur(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(c=>!e.terms[c]).reduce((c,p)=>[...c,O("del",null,p)," "],[]).slice(0,-1),i=ue(),s=new URL(e.location,i.base);te("search.highlight")&&s.searchParams.set("h",Object.entries(e.terms).filter(([,c])=>c).reduce((c,[p])=>`${c} ${p}`.trim(),""));let{tags:a}=ue();return O("a",{href:`${s}`,class:"md-search-result__link",tabIndex:-1},O("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&O("div",{class:"md-search-result__icon md-icon"}),r>0&&O("h1",null,e.title),r<=0&&O("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&e.tags.map(c=>{let p=a?c in a?`md-tag-icon md-tag--${a[c]}`:"md-tag-icon":"";return O("span",{class:`md-tag ${p}`},c)}),o>0&&n.length>0&&O("p",{class:"md-search-result__terms"},be("search.result.term.missing"),": ",...n)))}function mn(e){let t=e[0].score,r=[...e],o=ue(),n=r.findIndex(m=>!`${new URL(m.location,o.base)}`.includes("#")),[i]=r.splice(n,1),s=r.findIndex(m=>m.scoreUr(m,1)),...c.length?[O("details",{class:"md-search-result__more"},O("summary",{tabIndex:-1},O("div",null,c.length>0&&c.length===1?be("search.result.more.one"):be("search.result.more.other",c.length))),...c.map(m=>Ur(m,1)))]:[]];return O("li",{class:"md-search-result__item"},p)}function ln(e){return O("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>O("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?rr(r):r)))}function Nr(e){let t=`tabbed-control tabbed-control--${e}`;return O("div",{class:t,hidden:!0},O("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function fn(e){return O("div",{class:"md-typeset__scrollwrap"},O("div",{class:"md-typeset__table"},e))}function fa(e){let t=ue(),r=new URL(`../${e.version}/`,t.base);return O("li",{class:"md-version__item"},O("a",{href:`${r}`,class:"md-version__link"},e.title))}function un(e,t){return O("div",{class:"md-version"},O("button",{class:"md-version__current","aria-label":be("select.version")},t.title),O("ul",{class:"md-version__list"},e.map(fa)))}function ua(e){return e.tagName==="CODE"?z(".c, .c1, .cm",e):[e]}function da(e){let t=[];for(let r of ua(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let s;for(;s=/(\(\d+\))(!)?/.exec(i.textContent);){let[,a,c]=s;if(typeof c=="undefined"){let p=i.splitText(s.index);i=p.splitText(a.length),t.push(p)}else{i.textContent=a,t.push(i);break}}}}return t}function dn(e,t){t.append(...Array.from(e.childNodes))}function cr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,s=new Map;for(let a of da(t)){let[,c]=a.textContent.match(/\((\d+)\)/);ce(`:scope > li:nth-child(${c})`,e)&&(s.set(c,cn(c,i)),a.replaceWith(s.get(c)))}return s.size===0?T:$(()=>{let a=new x,c=a.pipe(J(),ee(!0)),p=[];for(let[m,f]of s)p.push([N(".md-typeset",f),N(`:scope > li:nth-child(${m})`,e)]);return o.pipe(K(c)).subscribe(m=>{e.hidden=!m,e.classList.toggle("md-annotation-list",m);for(let[f,u]of p)m?dn(f,u):dn(u,f)}),M(...[...s].map(([,m])=>sn(m,t,{target$:r}))).pipe(k(()=>a.complete()),le())})}function hn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return hn(t)}}function bn(e,t){return $(()=>{let r=hn(e);return typeof r!="undefined"?cr(r,e,t):T})}var gn=$t(Vr());var ha=0;function xn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return xn(t)}}function vn(e){return xe(e).pipe(l(({width:t})=>({scrollable:bt(e).width>t})),X("scrollable"))}function yn(e,t){let{matches:r}=matchMedia("(hover)"),o=$(()=>{let n=new x;if(n.subscribe(({scrollable:s})=>{s&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")}),gn.default.isSupported()&&(e.closest(".copy")||te("content.code.copy")&&!e.closest(".no-copy"))){let s=e.closest("pre");s.id=`__code_${ha++}`,s.insertBefore(pn(s.id),e)}let i=e.closest(".highlight");if(i instanceof HTMLElement){let s=xn(i);if(typeof s!="undefined"&&(i.classList.contains("annotate")||te("content.code.annotate"))){let a=cr(s,e,t);return vn(e).pipe(w(c=>n.next(c)),k(()=>n.complete()),l(c=>I({ref:e},c)),qe(xe(i).pipe(l(({width:c,height:p})=>c&&p),G(),v(c=>c?a:T))))}}return vn(e).pipe(w(s=>n.next(s)),k(()=>n.complete()),l(s=>I({ref:e},s)))});return te("content.lazy")?nr(e).pipe(L(n=>n),ge(1),v(()=>o)):o}function ba(e,{target$:t,print$:r}){let o=!0;return M(t.pipe(l(n=>n.closest("details:not([open])")),L(n=>e===n),l(()=>({action:"open",reveal:!0}))),r.pipe(L(n=>n||!o),w(()=>o=e.open),l(n=>({action:n?"open":"close"}))))}function En(e,t){return $(()=>{let r=new x;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),ba(e,t).pipe(w(o=>r.next(o)),k(()=>r.complete()),l(o=>I({ref:e},o)))})}var wn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var zr,ga=0;function xa(){return typeof mermaid=="undefined"||mermaid instanceof Element?ht("https://unpkg.com/mermaid@9.4.3/dist/mermaid.min.js"):H(void 0)}function Sn(e){return e.classList.remove("mermaid"),zr||(zr=xa().pipe(w(()=>mermaid.initialize({startOnLoad:!1,themeCSS:wn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),l(()=>{}),B(1))),zr.subscribe(()=>{e.classList.add("mermaid");let t=`__mermaid_${ga++}`,r=O("div",{class:"mermaid"}),o=e.textContent;mermaid.mermaidAPI.render(t,o,(n,i)=>{let s=r.attachShadow({mode:"closed"});s.innerHTML=n,e.replaceWith(r),i==null||i(s)})}),zr.pipe(l(()=>({ref:e})))}var Tn=O("table");function On(e){return e.replaceWith(Tn),Tn.replaceWith(fn(e)),H({ref:e})}function ya(e){let t=z(":scope > input",e),r=t.find(o=>o.checked)||t[0];return M(...t.map(o=>h(o,"change").pipe(l(()=>N(`label[for="${o.id}"]`))))).pipe(V(N(`label[for="${r.id}"]`)),l(o=>({active:o})))}function Mn(e,{viewport$:t}){let r=Nr("prev");e.append(r);let o=Nr("next");e.append(o);let n=N(".tabbed-labels",e);return $(()=>{let i=new x,s=i.pipe(J(),ee(!0));return Q([i,xe(e)]).pipe(Ae(1,Te),K(s)).subscribe({next([{active:a},c]){let p=Je(a),{width:m}=he(a);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${m}px`);let f=tr(n);(p.xf.x+c.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),Q([dt(n),xe(n)]).pipe(K(s)).subscribe(([a,c])=>{let p=bt(n);r.hidden=a.x<16,o.hidden=a.x>p.width-c.width-16}),M(h(r,"click").pipe(l(()=>-1)),h(o,"click").pipe(l(()=>1))).pipe(K(s)).subscribe(a=>{let{width:c}=he(n);n.scrollBy({left:c*a,behavior:"smooth"})}),te("content.tabs.link")&&i.pipe(je(1),oe(t)).subscribe(([{active:a},{offset:c}])=>{let p=a.innerText.trim();if(a.hasAttribute("data-md-switching"))a.removeAttribute("data-md-switching");else{let m=e.offsetTop-c.y;for(let u of z("[data-tabs]"))for(let d of z(":scope > input",u)){let b=N(`label[for="${d.id}"]`);if(b!==a&&b.innerText.trim()===p){b.setAttribute("data-md-switching",""),d.click();break}}window.scrollTo({top:e.offsetTop-m});let f=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...f])])}}),i.pipe(K(s)).subscribe(()=>{for(let a of z("audio, video",e))a.pause()}),ya(e).pipe(w(a=>i.next(a)),k(()=>i.complete()),l(a=>I({ref:e},a)))}).pipe(rt(ae))}function Ln(e,{viewport$:t,target$:r,print$:o}){return M(...z(".annotate:not(.highlight)",e).map(n=>bn(n,{target$:r,print$:o})),...z("pre:not(.mermaid) > code",e).map(n=>yn(n,{target$:r,print$:o})),...z("pre.mermaid",e).map(n=>Sn(n)),...z("table:not([class])",e).map(n=>On(n)),...z("details",e).map(n=>En(n,{target$:r,print$:o})),...z("[data-tabs]",e).map(n=>Mn(n,{viewport$:t})))}function Ea(e,{alert$:t}){return t.pipe(v(r=>M(H(!0),H(!1).pipe(ze(2e3))).pipe(l(o=>({message:r,active:o})))))}function _n(e,t){let r=N(".md-typeset",e);return $(()=>{let o=new x;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),Ea(e,t).pipe(w(n=>o.next(n)),k(()=>o.complete()),l(n=>I({ref:e},n)))})}function wa({viewport$:e}){if(!te("header.autohide"))return H(!1);let t=e.pipe(l(({offset:{y:n}})=>n),Ce(2,1),l(([n,i])=>[nMath.abs(i-n.y)>100),l(([,[n]])=>n),G()),o=We("search");return Q([e,o]).pipe(l(([{offset:n},i])=>n.y>400&&!i),G(),v(n=>n?r:H(!1)),V(!1))}function An(e,t){return $(()=>Q([xe(e),wa(t)])).pipe(l(([{height:r},o])=>({height:r,hidden:o})),G((r,o)=>r.height===o.height&&r.hidden===o.hidden),B(1))}function Cn(e,{header$:t,main$:r}){return $(()=>{let o=new x,n=o.pipe(J(),ee(!0));return o.pipe(X("active"),Ge(t)).subscribe(([{active:i},{hidden:s}])=>{e.classList.toggle("md-header--shadow",i&&!s),e.hidden=s}),r.subscribe(o),t.pipe(K(n),l(i=>I({ref:e},i)))})}function Sa(e,{viewport$:t,header$:r}){return sr(e,{viewport$:t,header$:r}).pipe(l(({offset:{y:o}})=>{let{height:n}=he(e);return{active:o>=n}}),X("active"))}function kn(e,t){return $(()=>{let r=new x;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=ce(".md-content h1");return typeof o=="undefined"?T:Sa(o,t).pipe(w(n=>r.next(n)),k(()=>r.complete()),l(n=>I({ref:e},n)))})}function Hn(e,{viewport$:t,header$:r}){let o=r.pipe(l(({height:i})=>i),G()),n=o.pipe(v(()=>xe(e).pipe(l(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),X("bottom"))));return Q([o,n,t]).pipe(l(([i,{top:s,bottom:a},{offset:{y:c},size:{height:p}}])=>(p=Math.max(0,p-Math.max(0,s-c,i)-Math.max(0,p+c-a)),{offset:s-i,height:p,active:s-i<=c})),G((i,s)=>i.offset===s.offset&&i.height===s.height&&i.active===s.active))}function Ta(e){let t=__md_get("__palette")||{index:e.findIndex(r=>matchMedia(r.getAttribute("data-md-color-media")).matches)};return H(...e).pipe(se(r=>h(r,"change").pipe(l(()=>r))),V(e[Math.max(0,t.index)]),l(r=>({index:e.indexOf(r),color:{scheme:r.getAttribute("data-md-color-scheme"),primary:r.getAttribute("data-md-color-primary"),accent:r.getAttribute("data-md-color-accent")}})),B(1))}function $n(e){let t=O("meta",{name:"theme-color"});document.head.appendChild(t);let r=O("meta",{name:"color-scheme"});return document.head.appendChild(r),$(()=>{let o=new x;o.subscribe(i=>{document.body.setAttribute("data-md-color-switching","");for(let[s,a]of Object.entries(i.color))document.body.setAttribute(`data-md-color-${s}`,a);for(let s=0;s{let i=ye("header"),s=window.getComputedStyle(i);return r.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(a=>(+a).toString(16).padStart(2,"0")).join("")})).subscribe(i=>t.content=`#${i}`),o.pipe(_e(ae)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")});let n=z("input",e);return Ta(n).pipe(w(i=>o.next(i)),k(()=>o.complete()),l(i=>I({ref:e},i)))})}var qr=$t(Vr());function Oa(e){e.setAttribute("data-md-copying","");let t=e.innerText;return e.removeAttribute("data-md-copying"),t}function Rn({alert$:e}){qr.default.isSupported()&&new j(t=>{new qr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Oa(N(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(w(t=>{t.trigger.focus()}),l(()=>be("clipboard.copied"))).subscribe(e)}function Ma(e){if(e.length<2)return[""];let[t,r]=[...e].sort((n,i)=>n.length-i.length).map(n=>n.replace(/[^/]+$/,"")),o=0;if(t===r)o=t.length;else for(;t.charCodeAt(o)===r.charCodeAt(o);)o++;return e.map(n=>n.replace(t.slice(0,o),""))}function pr(e){let t=__md_get("__sitemap",sessionStorage,e);if(t)return H(t);{let r=ue();return Jo(new URL("sitemap.xml",e||r.base)).pipe(l(o=>Ma(z("loc",o).map(n=>n.textContent))),pe(()=>T),He([]),w(o=>__md_set("__sitemap",o,sessionStorage,e)))}}function In({location$:e,viewport$:t}){let r=ue();if(location.protocol==="file:")return T;let o=pr().pipe(l(p=>p.map(m=>`${new URL(m,r.base)}`))),n=h(document.body,"click").pipe(oe(o),v(([p,m])=>{if(!(p.target instanceof Element))return T;let f=p.target.closest("a");if(f===null)return T;if(f.target||p.metaKey||p.ctrlKey)return T;let u=new URL(f.href);return u.search=u.hash="",m.includes(`${u}`)?(p.preventDefault(),H(new URL(f.href))):T}),le());n.pipe(ge(1)).subscribe(()=>{let p=ce("link[rel=icon]");typeof p!="undefined"&&(p.href=p.href)}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),n.pipe(oe(t)).subscribe(([p,{offset:m}])=>{history.scrollRestoration="manual",history.replaceState(m,""),history.pushState(null,"",p)}),n.subscribe(e);let i=e.pipe(V(fe()),X("pathname"),je(1),v(p=>ar(p).pipe(pe(()=>(ot(p),T))))),s=new DOMParser,a=i.pipe(v(p=>p.text()),v(p=>{let m=s.parseFromString(p,"text/html");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=outdated]","[data-md-component=logo]","[data-md-component=skip]",...te("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let d=ce(u),b=ce(u,m);typeof d!="undefined"&&typeof b!="undefined"&&d.replaceWith(b)}let f=ye("container");return Fe(z("script",f)).pipe(v(u=>{let d=m.createElement("script");if(u.src){for(let b of u.getAttributeNames())d.setAttribute(b,u.getAttribute(b));return u.replaceWith(d),new j(b=>{d.onload=()=>b.complete()})}else return d.textContent=u.textContent,u.replaceWith(d),T}),J(),ee(m))}),le());return h(window,"popstate").pipe(l(fe)).subscribe(e),e.pipe(V(fe()),Ce(2,1),v(([p,m])=>p.pathname===m.pathname&&p.hash!==m.hash?H(m):T)).subscribe(p=>{var m,f;history.state!==null||!p.hash?window.scrollTo(0,(f=(m=history.state)==null?void 0:m.y)!=null?f:0):(history.scrollRestoration="auto",Pr(p.hash),history.scrollRestoration="manual")}),a.pipe(oe(e)).subscribe(([,p])=>{var m,f;history.state!==null||!p.hash?window.scrollTo(0,(f=(m=history.state)==null?void 0:m.y)!=null?f:0):Pr(p.hash)}),a.pipe(v(()=>t),X("offset"),ke(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),a}var jn=$t(Fn());function Wn(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,s)=>`${i}${s}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return s=>(0,jn.default)(s).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function Lt(e){return e.type===1}function mr(e){return e.type===3}function Un(e,t){let r=on(e);return M(H(location.protocol!=="file:"),We("search")).pipe($e(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:te("search.suggest")}}})),r}function Nn({document$:e}){let t=ue(),r=Ue(new URL("../versions.json",t.base)).pipe(pe(()=>T)),o=r.pipe(l(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:s,aliases:a})=>s===i||a.includes(i))||n[0]}));r.pipe(l(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>h(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),oe(o),v(([i,s])=>{if(i.target instanceof Element){let a=i.target.closest("a");if(a&&!a.target&&n.has(a.href)){let c=a.href;return!i.target.closest(".md-version")&&n.get(c)===s?T:(i.preventDefault(),H(c))}}return T}),v(i=>{let{version:s}=n.get(i);return pr(new URL(i)).pipe(l(a=>{let p=fe().href.replace(t.base,"");return a.includes(p.split("#")[0])?new URL(`../${s}/${p}`,t.base):new URL(i)}))})))).subscribe(n=>ot(n)),Q([r,o]).subscribe(([n,i])=>{N(".md-header__topic").appendChild(un(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var s;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let a=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(a)||(a=[a]);e:for(let c of a)for(let p of n.aliases)if(new RegExp(c,"i").test(p)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let a of ne("outdated"))a.hidden=!1})}function ka(e,{worker$:t}){let{searchParams:r}=fe();r.has("q")&&(Ke("search",!0),e.value=r.get("q"),e.focus(),We("search").pipe($e(i=>!i)).subscribe(()=>{let i=new URL(location.href);i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=er(e),n=M(t.pipe($e(Lt)),h(e,"keyup"),o).pipe(l(()=>e.value),G());return Q([n,o]).pipe(l(([i,s])=>({value:i,focus:s})),B(1))}function Dn(e,{worker$:t}){let r=new x,o=r.pipe(J(),ee(!0));Q([t.pipe($e(Lt)),r],(i,s)=>s).pipe(X("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(X("focus")).subscribe(({focus:i})=>{i&&Ke("search",i)}),h(e.form,"reset").pipe(K(o)).subscribe(()=>e.focus());let n=N("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ka(e,{worker$:t}).pipe(w(i=>r.next(i)),k(()=>r.complete()),l(i=>I({ref:e},i)),B(1))}function Vn(e,{worker$:t,query$:r}){let o=new x,n=zo(e.parentElement).pipe(L(Boolean)),i=e.parentElement,s=N(":scope > :first-child",e),a=N(":scope > :last-child",e);We("search").subscribe(m=>a.setAttribute("role",m?"list":"presentation")),o.pipe(oe(r),Hr(t.pipe($e(Lt)))).subscribe(([{items:m},{value:f}])=>{switch(m.length){case 0:s.textContent=f.length?be("search.result.none"):be("search.result.placeholder");break;case 1:s.textContent=be("search.result.one");break;default:let u=rr(m.length);s.textContent=be("search.result.other",u)}});let c=o.pipe(w(()=>a.innerHTML=""),v(({items:m})=>M(H(...m.slice(0,10)),H(...m.slice(10)).pipe(Ce(4),Ir(n),v(([f])=>f)))),l(mn),le());return c.subscribe(m=>a.appendChild(m)),c.pipe(se(m=>{let f=ce("details",m);return typeof f=="undefined"?T:h(f,"toggle").pipe(K(o),l(()=>f))})).subscribe(m=>{m.open===!1&&m.offsetTop<=i.scrollTop&&i.scrollTo({top:m.offsetTop})}),t.pipe(L(mr),l(({data:m})=>m)).pipe(w(m=>o.next(m)),k(()=>o.complete()),l(m=>I({ref:e},m)))}function Ha(e,{query$:t}){return t.pipe(l(({value:r})=>{let o=fe();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function zn(e,t){let r=new x,o=r.pipe(J(),ee(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(K(o)).subscribe(n=>n.preventDefault()),Ha(e,t).pipe(w(n=>r.next(n)),k(()=>r.complete()),l(n=>I({ref:e},n)))}function qn(e,{worker$:t,keyboard$:r}){let o=new x,n=ye("search-query"),i=M(h(n,"keydown"),h(n,"focus")).pipe(_e(ae),l(()=>n.value),G());return o.pipe(Ge(i),l(([{suggest:a},c])=>{let p=c.split(/([\s-]+)/);if(a!=null&&a.length&&p[p.length-1]){let m=a[a.length-1];m.startsWith(p[p.length-1])&&(p[p.length-1]=m)}else p.length=0;return p})).subscribe(a=>e.innerHTML=a.join("").replace(/\s/g," ")),r.pipe(L(({mode:a})=>a==="search")).subscribe(a=>{switch(a.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(L(mr),l(({data:a})=>a)).pipe(w(a=>o.next(a)),k(()=>o.complete()),l(()=>({ref:e})))}function Kn(e,{index$:t,keyboard$:r}){let o=ue();try{let n=Un(o.search,t),i=ye("search-query",e),s=ye("search-result",e);h(e,"click").pipe(L(({target:c})=>c instanceof Element&&!!c.closest("a"))).subscribe(()=>Ke("search",!1)),r.pipe(L(({mode:c})=>c==="search")).subscribe(c=>{let p=Re();switch(c.type){case"Enter":if(p===i){let m=new Map;for(let f of z(":first-child [href]",s)){let u=f.firstElementChild;m.set(f,parseFloat(u.getAttribute("data-md-score")))}if(m.size){let[[f]]=[...m].sort(([,u],[,d])=>d-u);f.click()}c.claim()}break;case"Escape":case"Tab":Ke("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof p=="undefined")i.focus();else{let m=[i,...z(":not(details) > [href], summary, details[open] [href]",s)],f=Math.max(0,(Math.max(0,m.indexOf(p))+m.length+(c.type==="ArrowUp"?-1:1))%m.length);m[f].focus()}c.claim();break;default:i!==Re()&&i.focus()}}),r.pipe(L(({mode:c})=>c==="global")).subscribe(c=>{switch(c.type){case"f":case"s":case"/":i.focus(),i.select(),c.claim();break}});let a=Dn(i,{worker$:n});return M(a,Vn(s,{worker$:n,query$:a})).pipe(qe(...ne("search-share",e).map(c=>zn(c,{query$:a})),...ne("search-suggest",e).map(c=>qn(c,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ve}}function Qn(e,{index$:t,location$:r}){return Q([t,r.pipe(V(fe()),L(o=>!!o.searchParams.get("h")))]).pipe(l(([o,n])=>Wn(o.config)(n.searchParams.get("h"))),l(o=>{var s;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let a=i.nextNode();a;a=i.nextNode())if((s=a.parentElement)!=null&&s.offsetHeight){let c=a.textContent,p=o(c);p.length>c.length&&n.set(a,p)}for(let[a,c]of n){let{childNodes:p}=O("span",null,c);a.replaceWith(...Array.from(p))}return{ref:e,nodes:n}}))}function $a(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return Q([r,t]).pipe(l(([{offset:i,height:s},{offset:{y:a}}])=>(s=s+Math.min(n,Math.max(0,a-i))-n,{height:s,locked:a>=i+n})),G((i,s)=>i.height===s.height&&i.locked===s.locked))}function Kr(e,o){var n=o,{header$:t}=n,r=Zr(n,["header$"]);let i=N(".md-sidebar__scrollwrap",e),{y:s}=Je(i);return $(()=>{let a=new x,c=a.pipe(J(),ee(!0)),p=a.pipe(Ae(0,Te));return p.pipe(oe(t)).subscribe({next([{height:m},{height:f}]){i.style.height=`${m-2*s}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),p.pipe($e()).subscribe(()=>{for(let m of z(".md-nav__link--active[href]",e)){let f=or(m);if(typeof f!="undefined"){let u=m.offsetTop-f.offsetTop,{height:d}=he(f);f.scrollTo({top:u-d/2})}}}),me(z("label[tabindex]",e)).pipe(se(m=>h(m,"click").pipe(l(()=>m),K(c)))).subscribe(m=>{let f=N(`[id="${m.htmlFor}"]`);N(`[aria-labelledby="${m.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),$a(e,r).pipe(w(m=>a.next(m)),k(()=>a.complete()),l(m=>I({ref:e},m)))})}function Yn(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return Tt(Ue(`${r}/releases/latest`).pipe(pe(()=>T),l(o=>({version:o.tag_name})),He({})),Ue(r).pipe(pe(()=>T),l(o=>({stars:o.stargazers_count,forks:o.forks_count})),He({}))).pipe(l(([o,n])=>I(I({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return Ue(r).pipe(l(o=>({repositories:o.public_repos})),He({}))}}function Bn(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Ue(r).pipe(pe(()=>T),l(({star_count:o,forks_count:n})=>({stars:o,forks:n})),He({}))}function Gn(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return Yn(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return Bn(r,o)}return T}var Ra;function Ia(e){return Ra||(Ra=$(()=>{let t=__md_get("__source",sessionStorage);if(t)return H(t);if(ne("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return T}return Gn(e.href).pipe(w(o=>__md_set("__source",o,sessionStorage)))}).pipe(pe(()=>T),L(t=>Object.keys(t).length>0),l(t=>({facts:t})),B(1)))}function Jn(e){let t=N(":scope > :last-child",e);return $(()=>{let r=new x;return r.subscribe(({facts:o})=>{t.appendChild(ln(o)),t.classList.add("md-source__repository--active")}),Ia(e).pipe(w(o=>r.next(o)),k(()=>r.complete()),l(o=>I({ref:e},o)))})}function Pa(e,{viewport$:t,header$:r}){return xe(document.body).pipe(v(()=>sr(e,{header$:r,viewport$:t})),l(({offset:{y:o}})=>({hidden:o>=10})),X("hidden"))}function Xn(e,t){return $(()=>{let r=new x;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(te("navigation.tabs.sticky")?H({hidden:!1}):Pa(e,t)).pipe(w(o=>r.next(o)),k(()=>r.complete()),l(o=>I({ref:e},o)))})}function Fa(e,{viewport$:t,header$:r}){let o=new Map,n=z("[href^=\\#]",e);for(let a of n){let c=decodeURIComponent(a.hash.substring(1)),p=ce(`[id="${c}"]`);typeof p!="undefined"&&o.set(a,p)}let i=r.pipe(X("height"),l(({height:a})=>{let c=ye("main"),p=N(":scope > :first-child",c);return a+.8*(p.offsetTop-c.offsetTop)}),le());return xe(document.body).pipe(X("height"),v(a=>$(()=>{let c=[];return H([...o].reduce((p,[m,f])=>{for(;c.length&&o.get(c[c.length-1]).tagName>=f.tagName;)c.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return p.set([...c=[...c,m]].reverse(),u)},new Map))}).pipe(l(c=>new Map([...c].sort(([,p],[,m])=>p-m))),Ge(i),v(([c,p])=>t.pipe(Cr(([m,f],{offset:{y:u},size:d})=>{let b=u+d.height>=Math.floor(a.height);for(;f.length;){let[,_]=f[0];if(_-p=u&&!b)f=[m.pop(),...f];else break}return[m,f]},[[],[...c]]),G((m,f)=>m[0]===f[0]&&m[1]===f[1])))))).pipe(l(([a,c])=>({prev:a.map(([p])=>p),next:c.map(([p])=>p)})),V({prev:[],next:[]}),Ce(2,1),l(([a,c])=>a.prev.length{let i=new x,s=i.pipe(J(),ee(!0));if(i.subscribe(({prev:a,next:c})=>{for(let[p]of c)p.classList.remove("md-nav__link--passed"),p.classList.remove("md-nav__link--active");for(let[p,[m]]of a.entries())m.classList.add("md-nav__link--passed"),m.classList.toggle("md-nav__link--active",p===a.length-1)}),te("toc.follow")){let a=M(t.pipe(ke(1),l(()=>{})),t.pipe(ke(250),l(()=>"smooth")));i.pipe(L(({prev:c})=>c.length>0),Ge(o.pipe(_e(ae))),oe(a)).subscribe(([[{prev:c}],p])=>{let[m]=c[c.length-1];if(m.offsetHeight){let f=or(m);if(typeof f!="undefined"){let u=m.offsetTop-f.offsetTop,{height:d}=he(f);f.scrollTo({top:u-d/2,behavior:p})}}})}return te("navigation.tracking")&&t.pipe(K(s),X("offset"),ke(250),je(1),K(n.pipe(je(1))),Ot({delay:250}),oe(i)).subscribe(([,{prev:a}])=>{let c=fe(),p=a[a.length-1];if(p&&p.length){let[m]=p,{hash:f}=new URL(m.href);c.hash!==f&&(c.hash=f,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),Fa(e,{viewport$:t,header$:r}).pipe(w(a=>i.next(a)),k(()=>i.complete()),l(a=>I({ref:e},a)))})}function ja(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(l(({offset:{y:s}})=>s),Ce(2,1),l(([s,a])=>s>a&&a>0),G()),i=r.pipe(l(({active:s})=>s));return Q([i,n]).pipe(l(([s,a])=>!(s&&a)),G(),K(o.pipe(je(1))),ee(!0),Ot({delay:250}),l(s=>({hidden:s})))}function ei(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new x,s=i.pipe(J(),ee(!0));return i.subscribe({next({hidden:a}){e.hidden=a,a?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(K(s),X("height")).subscribe(({height:a})=>{e.style.top=`${a+16}px`}),h(e,"click").subscribe(a=>{a.preventDefault(),window.scrollTo({top:0})}),ja(e,{viewport$:t,main$:o,target$:n}).pipe(w(a=>i.next(a)),k(()=>i.complete()),l(a=>I({ref:e},a)))}function ti({document$:e,tablet$:t}){e.pipe(v(()=>z(".md-toggle--indeterminate")),w(r=>{r.indeterminate=!0,r.checked=!1}),se(r=>h(r,"change").pipe($r(()=>r.classList.contains("md-toggle--indeterminate")),l(()=>r))),oe(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function Wa(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function ri({document$:e}){e.pipe(v(()=>z("[data-md-scrollfix]")),w(t=>t.removeAttribute("data-md-scrollfix")),L(Wa),se(t=>h(t,"touchstart").pipe(l(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function oi({viewport$:e,tablet$:t}){Q([We("search"),t]).pipe(l(([r,o])=>r&&!o),v(r=>H(r).pipe(ze(r?400:100))),oe(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function Ua(){return location.protocol==="file:"?ht(`${new URL("search/search_index.js",Qr.base)}`).pipe(l(()=>__index),B(1)):Ue(new URL("search/search_index.json",Qr.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var nt=Wo(),At=Qo(),gt=Bo(At),Yr=Ko(),Se=rn(),lr=Fr("(min-width: 960px)"),ii=Fr("(min-width: 1220px)"),ai=Go(),Qr=ue(),si=document.forms.namedItem("search")?Ua():Ve,Br=new x;Rn({alert$:Br});te("navigation.instant")&&In({location$:At,viewport$:Se}).subscribe(nt);var ni;((ni=Qr.version)==null?void 0:ni.provider)==="mike"&&Nn({document$:nt});M(At,gt).pipe(ze(125)).subscribe(()=>{Ke("drawer",!1),Ke("search",!1)});Yr.pipe(L(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=ce("link[rel=prev]");typeof t!="undefined"&&ot(t);break;case"n":case".":let r=ce("link[rel=next]");typeof r!="undefined"&&ot(r);break;case"Enter":let o=Re();o instanceof HTMLLabelElement&&o.click()}});ti({document$:nt,tablet$:lr});ri({document$:nt});oi({viewport$:Se,tablet$:lr});var Xe=An(ye("header"),{viewport$:Se}),_t=nt.pipe(l(()=>ye("main")),v(e=>Hn(e,{viewport$:Se,header$:Xe})),B(1)),Na=M(...ne("consent").map(e=>an(e,{target$:gt})),...ne("dialog").map(e=>_n(e,{alert$:Br})),...ne("header").map(e=>Cn(e,{viewport$:Se,header$:Xe,main$:_t})),...ne("palette").map(e=>$n(e)),...ne("search").map(e=>Kn(e,{index$:si,keyboard$:Yr})),...ne("source").map(e=>Jn(e))),Da=$(()=>M(...ne("announce").map(e=>nn(e)),...ne("content").map(e=>Ln(e,{viewport$:Se,target$:gt,print$:ai})),...ne("content").map(e=>te("search.highlight")?Qn(e,{index$:si,location$:At}):T),...ne("header-title").map(e=>kn(e,{viewport$:Se,header$:Xe})),...ne("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?jr(ii,()=>Kr(e,{viewport$:Se,header$:Xe,main$:_t})):jr(lr,()=>Kr(e,{viewport$:Se,header$:Xe,main$:_t}))),...ne("tabs").map(e=>Xn(e,{viewport$:Se,header$:Xe})),...ne("toc").map(e=>Zn(e,{viewport$:Se,header$:Xe,main$:_t,target$:gt})),...ne("top").map(e=>ei(e,{viewport$:Se,header$:Xe,main$:_t,target$:gt})))),ci=nt.pipe(v(()=>Da),qe(Na),B(1));ci.subscribe();window.document$=nt;window.location$=At;window.target$=gt;window.keyboard$=Yr;window.viewport$=Se;window.tablet$=lr;window.screen$=ii;window.print$=ai;window.alert$=Br;window.component$=ci;})(); +//# sourceMappingURL=bundle.dff1b7c8.min.js.map + diff --git a/v13.1/assets/javascripts/workers/search.dfff1995.min.js b/v13.1/assets/javascripts/workers/search.dfff1995.min.js new file mode 100644 index 00000000..349360db --- /dev/null +++ b/v13.1/assets/javascripts/workers/search.dfff1995.min.js @@ -0,0 +1,42 @@ +"use strict";(()=>{var xe=Object.create;var U=Object.defineProperty,ve=Object.defineProperties,Se=Object.getOwnPropertyDescriptor,Te=Object.getOwnPropertyDescriptors,Qe=Object.getOwnPropertyNames,Y=Object.getOwnPropertySymbols,Ee=Object.getPrototypeOf,X=Object.prototype.hasOwnProperty,be=Object.prototype.propertyIsEnumerable;var Z=Math.pow,J=(t,e,r)=>e in t?U(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,A=(t,e)=>{for(var r in e||(e={}))X.call(e,r)&&J(t,r,e[r]);if(Y)for(var r of Y(e))be.call(e,r)&&J(t,r,e[r]);return t},G=(t,e)=>ve(t,Te(e));var Le=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var we=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Qe(e))!X.call(t,i)&&i!==r&&U(t,i,{get:()=>e[i],enumerable:!(n=Se(e,i))||n.enumerable});return t};var Pe=(t,e,r)=>(r=t!=null?xe(Ee(t)):{},we(e||!t||!t.__esModule?U(r,"default",{value:t,enumerable:!0}):r,t));var B=(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 te=Le((K,ee)=>{/** + * 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 + */(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";/*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + */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 l=t.utils.clone(r)||{};l.position=[a,c],l.index=s.length,s.push(new t.Token(n.slice(a,o),l))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */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?l+=2:a==u&&(r+=n[c+1]*i[l+1],c+=2,l+=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 l=s.str.charAt(0),g=s.str.charAt(1),f;g in s.node.edges?f=s.node.edges[g]:(f=new t.TokenSet,s.node.edges[g]=f),s.str.length==1&&(f.final=!0),i.push({node:f,editsRemaining:s.editsRemaining-1,str:l+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()}};/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */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 K=="object"?ee.exports=r():e.lunr=r()}(this,function(){return t})})()});var de=Pe(te());function re(t,e=document){let r=ke(t,e);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${t}" to be present`);return r}function ke(t,e=document){return e.querySelector(t)||void 0}Object.entries||(Object.entries=function(t){let e=[];for(let r of Object.keys(t))e.push([r,t[r]]);return e});Object.values||(Object.values=function(t){let e=[];for(let r of Object.keys(t))e.push(t[r]);return e});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(t,e){typeof t=="object"?(this.scrollLeft=t.left,this.scrollTop=t.top):(this.scrollLeft=t,this.scrollTop=e)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...t){let e=this.parentNode;if(e){t.length===0&&e.removeChild(this);for(let r=t.length-1;r>=0;r--){let n=t[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?e.insertBefore(this.previousSibling,n):e.replaceChild(n,this)}}}));function ne(t){let e=new Map;for(let r of t){let[n]=r.location.split("#"),i=e.get(n);typeof i=="undefined"?e.set(n,r):(e.set(r.location,r),r.parent=i)}return e}function W(t,e,r){var s;e=new RegExp(e,"g");let n,i=0;do{n=e.exec(t);let o=(s=n==null?void 0:n.index)!=null?s:t.length;if(in?e(r,1,n,n=i):t.charAt(i)===">"&&(t.charAt(n+1)==="/"?--s===0&&e(r++,2,n,i+1):t.charAt(i-1)!=="/"&&s++===0&&e(r,0,n,i+1),n=i+1);i>n&&e(r,1,n,i)}function se(t,e,r){return q([t],e,r).pop()}function q(t,e,r){let n=[0];for(let i=1;i>>2&1023,u=o[0]>>>12;n.push(+(a>u)+n[n.length-1])}return t.map((i,s)=>{let o=new Map;for(let u of r.sort((c,l)=>c-l)){let c=u&1048575,l=u>>>20;if(n[l]!==s)continue;let g=o.get(l);typeof g=="undefined"&&o.set(l,g=[]),g.push(c)}if(o.size===0)return i;let a=[];for(let[u,c]of o){let l=e[u],g=l[0]>>>12,f=l[l.length-1]>>>12,v=l[l.length-1]>>>2&1023,m=i.slice(g,f+v);for(let x of c.sort((d,y)=>y-d)){let d=(l[x]>>>12)-g,y=(l[x]>>>2&1023)+d;m=[m.slice(0,d),"",m.slice(d,y),"",m.slice(y)].join("")}if(a.push(m)===2)break}return a.join("")})}function oe(t){let e=[];if(typeof t=="undefined")return e;let r=Array.isArray(t)?t:[t];for(let n=0;n{var l;switch(i[l=o+=s]||(i[l]=[]),a){case 0:case 2:i[o].push(u<<12|c-u<<2|a);break;case 1:let g=r[n].slice(u,c);W(g,lunr.tokenizer.separator,(f,v)=>{if(typeof lunr.segmenter!="undefined"){let m=g.slice(f,v);if(/^[MHIK]$/.test(lunr.segmenter.ctype_(m))){let x=lunr.segmenter.segment(m);for(let d=0,y=0;dr){return t.trim().split(/"([^"]+)"/g).map((r,n)=>n&1?r.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g," +"):r).join("").replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g,"").split(/\s+/g).reduce((r,n)=>{let i=e(n);return[...r,...Array.isArray(i)?i:[i]]},[]).map(r=>/([~^]$)/.test(r)?`${r}1`:r).map(r=>/(^[+-]|[~^]\d+$)/.test(r)?r:`${r}*`).join(" ")}function ue(t){return ae(t,e=>{let r=[],n=new lunr.QueryLexer(e);n.run();for(let{type:i,str:s,start:o,end:a}of n.lexemes)switch(i){case"FIELD":["title","text","tags"].includes(s)||(e=[e.slice(0,a)," ",e.slice(a+1)].join(""));break;case"TERM":W(s,lunr.tokenizer.separator,(...u)=>{r.push([e.slice(0,o),s.slice(...u),e.slice(a)].join(""))})}return r})}function ce(t){let e=new lunr.Query(["title","text","tags"]);new lunr.QueryParser(t,e).parse();for(let n of e.clauses)n.usePipeline=!0,n.term.startsWith("*")&&(n.wildcard=lunr.Query.wildcard.LEADING,n.term=n.term.slice(1)),n.term.endsWith("*")&&(n.wildcard=lunr.Query.wildcard.TRAILING,n.term=n.term.slice(0,-1));return e.clauses}function le(t,e){var i;let r=new Set(t),n={};for(let s=0;s0;){let o=i[--s];for(let u=1;un[o]-u&&(r.add(t.slice(o,o+u)),i[s++]=o+u);let a=o+n[o];n[a]&&ar=>{if(typeof r[e]=="undefined")return;let n=[r.location,e].join(":");return t.set(n,lunr.tokenizer.table=[]),r[e]}}function Re(t,e){let[r,n]=[new Set(t),new Set(e)];return[...new Set([...r].filter(i=>!n.has(i)))]}var H=class{constructor({config:e,docs:r,options:n}){let i=Oe(this.table=new Map);this.map=ne(r),this.options=n,this.index=lunr(function(){this.metadataWhitelist=["position"],this.b(0),e.lang.length===1&&e.lang[0]!=="en"?this.use(lunr[e.lang[0]]):e.lang.length>1&&this.use(lunr.multiLanguage(...e.lang)),this.tokenizer=oe,lunr.tokenizer.separator=new RegExp(e.separator),lunr.segmenter="TinySegmenter"in lunr?new lunr.TinySegmenter:void 0;let s=Re(["trimmer","stopWordFilter","stemmer"],e.pipeline);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.ref("location"),this.field("title",{boost:1e3,extractor:i("title")}),this.field("text",{boost:1,extractor:i("text")}),this.field("tags",{boost:1e6,extractor:i("tags")});for(let o of r)this.add(o,{boost:o.boost})})}search(e){if(e=e.replace(new RegExp("\\p{sc=Han}+","gu"),s=>[...he(s,this.index.invertedIndex)].join("* ")),e=ue(e),!e)return{items:[]};let r=ce(e).filter(s=>s.presence!==lunr.Query.presence.PROHIBITED),n=this.index.search(e).reduce((s,{ref:o,score:a,matchData:u})=>{let c=this.map.get(o);if(typeof c!="undefined"){c=A({},c),c.tags&&(c.tags=[...c.tags]);let l=le(r,Object.keys(u.metadata));for(let f of this.index.fields){if(typeof c[f]=="undefined")continue;let v=[];for(let d of Object.values(u.metadata))typeof d[f]!="undefined"&&v.push(...d[f].position);if(!v.length)continue;let m=this.table.get([c.location,f].join(":")),x=Array.isArray(c[f])?q:se;c[f]=x(c[f],m,v)}let g=+!c.parent+Object.values(l).filter(f=>f).length/Object.keys(l).length;s.push(G(A({},c),{score:a*(1+Z(g,2)),terms:l}))}return s},[]).sort((s,o)=>o.score-s.score).reduce((s,o)=>{let a=this.map.get(o.location);if(typeof a!="undefined"){let u=a.parent?a.parent.location:a.location;s.set(u,[...s.get(u)||[],o])}return s},new Map);for(let[s,o]of n)if(!o.find(a=>a.location===s)){let a=this.map.get(s);o.push(G(A({},a),{score:0,terms:{}}))}let i;if(this.options.suggest){let s=this.index.query(o=>{for(let a of r)o.term(a.term,{fields:["title"],presence:lunr.Query.presence.REQUIRED,wildcard:lunr.Query.wildcard.TRAILING})});i=s.length?Object.keys(s[0].matchData.metadata):[]}return A({items:[...n.values()]},typeof i!="undefined"&&{suggest:i})}};var fe;function Ie(t){return B(this,null,function*(){let e="../lunr";if(typeof parent!="undefined"&&"IFrameWorker"in parent){let n=re("script[src]"),[i]=n.src.split("/worker");e=e.replace("..",i)}let r=[];for(let n of t.lang){switch(n){case"ja":r.push(`${e}/tinyseg.js`);break;case"hi":case"th":r.push(`${e}/wordcut.js`);break}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 Fe(t){return B(this,null,function*(){switch(t.type){case 0:return yield Ie(t.data.config),fe=new H(t.data),{type:1};case 2:let e=t.data;try{return{type:3,data:fe.search(e)}}catch(r){return console.warn(`Invalid query: ${e} \u2013 see https://bit.ly/2s3ChXG`),console.warn(r),{type:3,data:{items:[]}}}default:throw new TypeError("Invalid message type")}})}self.lunr=de.default;addEventListener("message",t=>B(void 0,null,function*(){postMessage(yield Fe(t.data))}));})(); +//# sourceMappingURL=search.dfff1995.min.js.map + diff --git a/v13.1/assets/logo/dmo-logo-white.min.svg b/v13.1/assets/logo/dmo-logo-white.min.svg new file mode 100644 index 0000000000000000000000000000000000000000..7ec0dbb32e5746bbf2a6c626cec75b23094a61df GIT binary patch literal 677 zcmZuv+fD*O4E>d6eeUe^HWvdt_~e6p1W^%^Rf!Om`1N)`1)}LT(`om#XWCA-`xn@+ zpU1oR!?fcFtO`clw1dOz@V1?HtKDvM+xO!*hEc-%$4k#K#@?abY_eJGR`4|K=EMwI z|nVbhG}>6EKR!pT?D(n#$?q>Jg@j zxd2=7w-hkO0@+(}e4UzSYK(|bJ&tankP}058ivlNq6lh6F;veHT2q3cgzRTYPAd64 pL;0MO_^PNb*!k0O)JNlV{qf-M^}=&@wgqV@_nZgy-O#pVvoB`klAHhl literal 0 HcmV?d00001 diff --git a/v13.1/assets/logo/dmo-logo.min.svg b/v13.1/assets/logo/dmo-logo.min.svg new file mode 100644 index 0000000000000000000000000000000000000000..72c98a5aa6d8d4f385fb449d62de61114269f219 GIT binary patch literal 1084 zcmZuw(Qex?4Ez121`86_v(yBJ~NHCvxS9Dl9v>U=Ab) zrU=-HojuGcchgG`Zw$LVNe4BMK;lFN%Ba9iC=pXuz{cnXwx@|0YQ#31^&lh_03kae zy@pzx9;?F`6(9rgL=QAc8U=Er4Ecn*m~+A>DHTLCA6h8bpb#0FlVN2vCYqcb|MjfN zbmOys(q!$VDFG$Sfu+zCt^AKB4NVzh!bz;>vSsOT55-#O2H(Gdo`CgfVw?v|2`@q# z=sCpV{8D@X2zqel=nyndSTMy~3PW%=;8Yj@1^dTM-yMFfhf#um-#zu^jykThWZdD7 o6)!%}6YjUXU#RmTUM3heoD#g963ST1d&)e|QDB7*kGp^U2VhI=Y5)KL literal 0 HcmV?d00001 diff --git a/v13.1/assets/logo/favicon-32x32.png b/v13.1/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/v13.1/assets/stylesheets/main.046329b4.min.css b/v13.1/assets/stylesheets/main.046329b4.min.css new file mode 100644 index 00000000..77ac5aae --- /dev/null +++ b/v13.1/assets/stylesheets/main.046329b4.min.css @@ -0,0 +1 @@ +@charset "UTF-8";html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}@media (prefers-reduced-motion){*,:after,:before{transition:none!important}}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:initial;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:initial;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:#0000;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root{--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:#ffffffb3;--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-scheme=default]{color-scheme:light}[data-md-color-scheme=default] img[src$="#gh-dark-mode-only"],[data-md-color-scheme=default] img[src$="#only-dark"]{display:none}:root,[data-md-color-scheme=default]{--md-default-fg-color:#000000de;--md-default-fg-color--light:#0000008a;--md-default-fg-color--lighter:#00000052;--md-default-fg-color--lightest:#00000012;--md-default-bg-color:#fff;--md-default-bg-color--light:#ffffffb3;--md-default-bg-color--lighter:#ffffff4d;--md-default-bg-color--lightest:#ffffff1f;--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-hl-color:#ffff0080;--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:#ffff0080;--md-typeset-del-color:#f5503d26;--md-typeset-ins-color:#0bd57026;--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-typeset-table-color:#0000001f;--md-typeset-table-color--light:rgba(0,0,0,.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-warning-fg-color:#000000de;--md-warning-bg-color:#ff9;--md-footer-fg-color:#fff;--md-footer-fg-color--light:#ffffffb3;--md-footer-fg-color--lighter:#ffffff73;--md-footer-bg-color:#000000de;--md-footer-bg-color--dark:#00000052;--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #0000001a,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0003,0 0 0.05rem #00000059}.md-icon svg{fill:currentcolor;display:block;height:1.2rem;width:1.2rem}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-text-font-family:var(--md-text-font,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;--md-code-font-family:var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,monospace}aside,body,input{font-feature-settings:"kern","liga";color:var(--md-typeset-color);font-family:var(--md-text-font-family)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family)}:root{--md-typeset-table-sort-icon:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--desc: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-bottom:1em;margin-top:1em}.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 code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent)}.md-typeset a code{color:currentcolor;transition:background-color 125ms}.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;font-variant-ligatures:none}@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 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;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.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)}.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[title]:focus:after,.md-typeset abbr[title]:hover:after{background-color:var(--md-default-fg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z3);color:var(--md-default-bg-color);content:attr(title);font-size:.7rem;left:.8rem;margin-top:2em;padding:.2rem .3rem;position:absolute;right:.8rem}}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.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}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.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}[dir=ltr] .md-typeset ol li ol,[dir=ltr] .md-typeset ol li ul,[dir=ltr] .md-typeset ul li ol,[dir=ltr] .md-typeset ul li ul{margin-left:.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-right:.625em}.md-typeset ol li ol,.md-typeset ol li ul,.md-typeset ul li ol,.md-typeset ul li ul{margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg,.md-typeset video{height:auto;max-width:100%}.md-typeset img[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child{margin-top:0}.md-typeset figure{display:flow-root;margin:1em 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;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;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{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tbody tr{transition:background-color 125ms}.md-typeset table:not([class]) tbody tr:hover{background-color:var(--md-typeset-table-color--light);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.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%}@media screen and (max-width:44.9375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-typeset .md-author{display:block;flex-shrink:0;height:1.6rem;overflow:hidden;position:relative;transition:color 125ms,transform 125ms;width:1.6rem}.md-typeset .md-author img{border-radius:100%;display:block}.md-typeset .md-author--more{background:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--lighter);font-size:.6rem;font-weight:700;line-height:1.6rem;text-align:center}.md-typeset .md-author--long{height:2.4rem;width:2.4rem}.md-typeset a.md-author{transform:scale(1)}.md-typeset a.md-author img{filter:grayscale(100%) opacity(75%);transition:filter 125ms}.md-typeset a.md-author:focus,.md-typeset a.md-author:hover{transform:scale(1.1);z-index:1}.md-typeset a.md-author:focus img,.md-typeset a.md-author:hover img{filter:grayscale(0)}.md-banner{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background-color:var(--md-warning-bg-color);color:var(--md-warning-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}[dir=ltr] .md-banner__button{float:right}[dir=rtl] .md-banner__button{float:left}.md-banner__button{color:inherit;cursor:pointer;transition:opacity .25s}.no-js .md-banner__button{display:none}.md-banner__button:hover{opacity:.7}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-scrolllock]{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}.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}: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-position:center;mask-position:center;-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)}@keyframes consent{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes overlay{0%{opacity:0}to{opacity:1}}.md-consent__overlay{animation:overlay .25s both;-webkit-backdrop-filter:blur(.1rem);backdrop-filter:blur(.1rem);background-color:#0000008a;height:100%;opacity:1;position:fixed;top:0;width:100%;z-index:5}.md-consent__inner{animation:consent .5s cubic-bezier(.1,.7,.1,1) both;background-color:var(--md-default-bg-color);border:0;border-radius:.1rem;bottom:0;box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;max-height:100%;overflow:auto;padding:0;position:fixed;width:100%;z-index:5}.md-consent__form{padding:.8rem}.md-consent__settings{display:none;margin:1em 0}input:checked+.md-consent__settings{display:block}.md-consent__controls{margin-bottom:.8rem}.md-typeset .md-consent__controls .md-button{display:inline}@media screen and (max-width:44.9375em){.md-typeset .md-consent__controls .md-button{display:block;margin-top:.4rem;text-align:center;width:100%}}.md-consent label{cursor:pointer}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary: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}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{float:right}[dir=rtl] .md-content__button{float:left}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{margin:.4rem 0;padding:0}@media print{.md-content__button{display:none}}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-default-fg-color);border-radius:.1rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem .6rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog--active{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-feedback{margin:2em 0 1em;text-align:center}.md-feedback fieldset{border:none;margin:0;padding:0}.md-feedback__title{font-weight:700;margin:1em auto}.md-feedback__inner{position:relative}.md-feedback__list{align-content:baseline;display:flex;flex-wrap:wrap;justify-content:center;position:relative}.md-feedback__list:hover .md-icon:not(:disabled){color:var(--md-default-fg-color--lighter)}:disabled .md-feedback__list{min-height:1.8rem}.md-feedback__icon{color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;margin:0 .1rem;transition:color 125ms}.md-feedback__icon:not(:disabled).md-icon:hover{color:var(--md-accent-fg-color)}.md-feedback__icon:disabled{color:var(--md-default-fg-color--lightest);pointer-events:none}.md-feedback__note{opacity:0;position:relative;transform:translateY(.4rem);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-feedback__note>*{margin:0 auto;max-width:16rem}:disabled .md-feedback__note{opacity:1;transform:translateY(0)}.md-footer{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__inner:not([hidden]){display:flex}.md-footer__link{align-items:end;display:flex;flex-grow:0.01;margin-bottom:.4rem;margin-top:1rem;max-width:100%;outline-color:var(--md-accent-fg-color);overflow:hidden;transition:opacity .25s}.md-footer__link:focus,.md-footer__link:hover{opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.9375em){.md-footer__link--prev{flex-shrink:0}.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.9rem;margin-bottom:.7rem;max-width:calc(100% - 2.4rem);padding:0 1rem;white-space:nowrap}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.64rem;opacity:.7}.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-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-copyright{width:auto}}.md-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-social{display:inline-flex;gap:.2rem;margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-typeset .md-button{border:.1rem solid;border-radius:.1rem;color:var(--md-primary-fg-color);cursor:pointer;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)}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:focus,.md-typeset .md-input:hover{border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem #0000,0 .2rem .4rem #0000;color:var(--md-primary-bg-color);display:block;left:0;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1),box-shadow .25s}.md-header--shadow{box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;transition:transform .25s cubic-bezier(.1,.7,.1,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:auto}@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;white-space:nowrap}.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__topic:first-child{font-weight:700}[dir=ltr] .md-header__title{margin-left:1rem}[dir=rtl] .md-header__title{margin-right:1rem}[dir=ltr] .md-header__title{margin-right:.4rem}[dir=rtl] .md-header__title{margin-left:.4rem}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;line-height:2.4rem}.md-header__title--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--active .md-header__topic{transform:translateX(1.25rem)}.md-header__title--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__option>input{bottom:0}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.7rem;width:11.7rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-meta{color:var(--md-default-fg-color--light);font-size:.7rem;line-height:1.3}.md-meta__list{display:inline-flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}.md-meta__item:not(:last-child):after{content:"·";margin-left:.2rem;margin-right:.2rem}.md-meta__link{color:var(--md-typeset-a-color)}.md-meta__link:focus,.md-meta__link:hover{color:var(--md-accent-fg-color)}.md-draft{background-color:#ff1744;border-radius:.125em;color:#fff;display:inline-block;font-weight:700;padding-left:.5714285714em;padding-right:.5714285714em}: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{color:var(--md-default-fg-color--light);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;max-width:100%;object-fit:contain;width:auto}.md-nav__list{list-style:none;margin:0;padding:0}.md-nav__item{padding:0 .6rem}[dir=ltr] .md-nav__item .md-nav__item{padding-right:0}[dir=rtl] .md-nav__item .md-nav__item{padding-left:0}.md-nav__link{align-items:flex-start;display:flex;margin-top:.625em;scroll-snap-align:start;transition:color 125ms}.md-nav__link--passed{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active,.md-nav__item .md-nav__link--active code{color:var(--md-typeset-a-color)}.md-nav__link .md-ellipsis{position:relative}.md-nav__link .md-icon:last-child{margin-left:auto}.md-nav__link svg{fill:currentcolor;flex-shrink:0;height:1.3em}[dir=ltr] .md-nav__link svg+*{margin-left:.4rem}[dir=rtl] .md-nav__link svg+*{margin-right:.4rem}.md-nav__link:not(.md-nav__container):focus,.md-nav__link:not(.md-nav__container):hover{color:var(--md-accent-fg-color);cursor:pointer}.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__container>.md-nav__link{margin-top:0}.md-nav__container>.md-nav__link:first-child{flex-grow:1}.md-nav__icon{flex-shrink:0}.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;height:5.6rem;line-height:2.4rem;padding:3rem .8rem .2rem;position:relative;white-space:nowrap}[dir=ltr] .md-nav--primary .md-nav__title .md-nav__icon{left:.4rem}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem}.md-nav--primary .md-nav__title .md-nav__icon{display:block;height:1.2rem;margin:.2rem;position:absolute;top:.4rem;width:1.2rem}.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-position:center;mask-position:center;-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;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);font-weight:700}.md-nav--primary .md-nav__title .md-logo{display:block;left:.2rem;margin:.2rem;padding:.4rem;position:absolute;right:.2rem;top:.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--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}.md-nav--primary .md-nav__link svg{margin-top:.1em}.md-nav--primary .md-nav__link>.md-nav__link{padding:0}[dir=ltr] .md-nav--primary .md-nav__link .md-nav__icon{margin-right:-.2rem}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{margin-left:-.2rem}.md-nav--primary .md-nav__link .md-nav__icon{font-size:1.2rem;height:1.2rem;width:1.2rem}.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-position:center;mask-position:center;-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{background-color:initial;position:static}[dir=ltr] .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-right:1.4rem}[dir=ltr] .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-right:2rem}[dir=ltr] .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-right:2.6rem}[dir=ltr] .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-right:3.2rem}.md-nav--secondary{background-color:initial}.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:flex}.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:flex}.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{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.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{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon,.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{font-weight:700}.md-nav__item--section>.md-nav__link[for]{color:var(--md-default-fg-color--light)}.md-nav__item--section>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav__item--section>.md-nav__link .md-nav__icon{display:none}.md-nav__item--section>.md-nav{display:block}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav__icon{border-radius:100%;height:.9rem;transition:background-color .25s;width:.9rem}.md-nav__icon:hover{background-color:var(--md-accent-fg-color--transparent)}.md-nav__icon:after{background-color:currentcolor;border-radius:100%;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;vertical-align:-.1rem;width:100%}[dir=rtl] .md-nav__icon:after{transform:rotate(180deg)}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon:after,.md-nav__item--nested .md-nav__toggle:indeterminate~.md-nav__link .md-nav__icon:after{transform:rotate(90deg)}.md-nav--lifted>.md-nav__list>.md-nav__item,.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--active{display:block;padding:0}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);font-weight:700;margin-top:0;padding:0 .6rem;position:sticky;top:0;z-index:1}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link .md-nav__icon{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item>[for]{color:var(--md-default-fg-color--light)}.md-nav--lifted .md-nav[data-md-level="1"]{display:block}[dir=ltr] .md-nav--lifted .md-nav[data-md-level="1"]>.md-nav__list>.md-nav__item{padding-right:.6rem}[dir=rtl] .md-nav--lifted .md-nav[data-md-level="1"]>.md-nav__list>.md-nav__item{padding-left:.6rem}.md-nav--integrated>.md-nav__list>.md-nav__item--active:not(.md-nav__item--nested){padding:0 .6rem}.md-nav--integrated>.md-nav__list>.md-nav__item--active:not(.md-nav__item--nested)>.md-nav__link{padding:0}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-primary-fg-color)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-primary-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:1.25em}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}}.md-pagination{font-size:.8rem;font-weight:700;gap:.4rem}.md-pagination,.md-pagination>*{align-items:center;display:flex;justify-content:center}.md-pagination>*{border-radius:.2rem;height:1.8rem;min-width:1.8rem;text-align:center}.md-pagination__current{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light)}.md-pagination__link{transition:color 125ms,background-color 125ms}.md-pagination__link:focus,.md-pagination__link:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-pagination__link:focus svg,.md-pagination__link:hover svg{color:var(--md-accent-fg-color)}.md-pagination__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-pagination__link svg{fill:currentcolor;color:var(--md-default-fg-color--lighter);display:block;max-height:100%;width:1.2rem}.md-post__back{border-bottom:.05rem solid var(--md-default-fg-color--lightest);margin-bottom:1.2rem;padding-bottom:1.2rem}@media screen and (max-width:76.1875em){.md-post__back{display:none}}[dir=rtl] .md-post__back svg{transform:scaleX(-1)}.md-post__authors{display:flex;flex-direction:column;gap:.6rem;margin:0 .6rem}.md-post .md-post__meta a{transition:color 125ms}.md-post .md-post__meta a:focus,.md-post .md-post__meta a:hover{color:var(--md-accent-fg-color)}.md-post--excerpt{margin-bottom:3.2rem}.md-post--excerpt .md-post__header{align-items:center;display:flex;gap:.6rem;min-height:1.6rem}.md-post--excerpt .md-post__authors{align-items:center;display:inline-flex;flex-direction:row;gap:.2rem;margin:0;min-height:2.4rem}[dir=ltr] .md-post--excerpt .md-post__meta .md-meta__list{margin-right:.4rem}[dir=rtl] .md-post--excerpt .md-post__meta .md-meta__list{margin-left:.4rem}.md-post--excerpt .md-post__content>:first-child{--md-scroll-margin:6rem;margin-top:0}.md-post>.md-nav--secondary,.md-post>.md-nav:first-child>.md-nav__list{margin:1em 0}.md-profile{align-items:center;display:flex;font-size:.7rem;gap:.6rem;line-height:1.4;width:100%}.md-profile__description{flex-grow:1}.md-content--post{display:flex}@media screen and (max-width:76.1875em){.md-content--post{flex-flow:column-reverse}}.md-content--post>.md-content__inner{min-width:0}@media screen and (min-width:76.25em){[dir=ltr] .md-content--post>.md-content__inner{margin-left:1.2rem}[dir=rtl] .md-content--post>.md-content__inner{margin-right:1.2rem}}@media screen and (max-width:76.1875em){.md-sidebar.md-sidebar--post{padding:0}}: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){[dir=ltr] .md-search__overlay{left:-2.2rem}[dir=rtl] .md-search__overlay{right:-2.2rem}.md-search__overlay{background-color:var(--md-default-bg-color);border-radius:1rem;height:2rem;overflow:hidden;pointer-events:none;position:absolute;top:-1rem;transform-origin:center;transition:transform .3s .1s,opacity .2s .2s;width:2rem}[data-md-toggle=search]:checked~.md-header .md-search__overlay{opacity:1;transition:transform .4s,opacity .1s}}@media screen and (min-width:60em){[dir=ltr] .md-search__overlay{left:0}[dir=rtl] .md-search__overlay{right:0}.md-search__overlay{background-color:#0000008a;cursor:pointer;height:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width: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){[dir=ltr] .md-search__inner{left:0}[dir=rtl] .md-search__inner{right:0}.md-search__inner{height:0;opacity:0;overflow:hidden;position:fixed;top:0;transform:translateX(5%);transition:width 0ms .3s,height 0ms .3s,transform .15s cubic-bezier(.4,0,.2,1) .15s,opacity .15s .15s;width:0;z-index:2}[dir=rtl] .md-search__inner{transform:translateX(-5%)}[data-md-toggle=search]:checked~.md-header .md-search__inner{height:100%;opacity:1;transform:translateX(0);transition:width 0ms 0ms,height 0ms 0ms,transform .15s cubic-bezier(.1,.7,.1,1) .15s,opacity .15s .15s;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__inner{float:right}[dir=rtl] .md-search__inner{float:left}.md-search__inner{padding:.1rem 0;position:relative;transition:width .25s cubic-bezier(.1,.7,.1,1);width:11.7rem}}@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{background-color:var(--md-default-bg-color);box-shadow:0 0 .6rem #0000;height:2.4rem;position:relative;transition:color .25s,background-color .25s;z-index:2}@media screen and (min-width:60em){.md-search__form{background-color:#00000042;border-radius:.1rem;height:1.8rem}.md-search__form:hover{background-color:#ffffff1f}}[data-md-toggle=search]:checked~.md-header .md-search__form{background-color:var(--md-default-bg-color);border-radius:.1rem .1rem 0 0;box-shadow:0 0 .6rem #00000012;color:var(--md-default-fg-color)}[dir=ltr] .md-search__input{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__input{padding-left:2.2rem;padding-right:3.6rem}.md-search__input{background:#0000;font-size:.9rem;height:100%;position:relative;text-overflow:ellipsis;width:100%;z-index:2}.md-search__input::placeholder{transition:color .25s}.md-search__input::placeholder,.md-search__input~.md-search__icon{color:var(--md-default-fg-color--light)}.md-search__input::-ms-clear{display:none}@media screen and (max-width:59.9375em){.md-search__input{font-size:.9rem;height:2.4rem;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__input{padding-left:2.2rem}[dir=rtl] .md-search__input{padding-right:2.2rem}.md-search__input{color:inherit;font-size:.8rem}.md-search__input::placeholder{color:var(--md-primary-bg-color--light)}.md-search__input+.md-search__icon{color:var(--md-primary-bg-color)}[data-md-toggle=search]:checked~.md-header .md-search__input{text-overflow:clip}[data-md-toggle=search]:checked~.md-header .md-search__input+.md-search__icon{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input::placeholder{color:#0000}}.md-search__icon{cursor:pointer;display:inline-block;height:1.2rem;transition:color .25s,opacity .25s;width:1.2rem}.md-search__icon:hover{opacity:.7}[dir=ltr] .md-search__icon[for=__search]{left:.5rem}[dir=rtl] .md-search__icon[for=__search]{right:.5rem}.md-search__icon[for=__search]{position:absolute;top:.3rem;z-index:2}[dir=rtl] .md-search__icon[for=__search] svg{transform:scaleX(-1)}@media screen and (max-width:59.9375em){[dir=ltr] .md-search__icon[for=__search]{left:.8rem}[dir=rtl] .md-search__icon[for=__search]{right:.8rem}.md-search__icon[for=__search]{top:.6rem}.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}}[dir=ltr] .md-search__options{right:.5rem}[dir=rtl] .md-search__options{left:.5rem}.md-search__options{pointer-events:none;position:absolute;top:.3rem;z-index:2}@media screen and (max-width:59.9375em){[dir=ltr] .md-search__options{right:.8rem}[dir=rtl] .md-search__options{left:.8rem}.md-search__options{top:.6rem}}[dir=ltr] .md-search__options>.md-icon{margin-left:.2rem}[dir=rtl] .md-search__options>.md-icon{margin-right:.2rem}.md-search__options>.md-icon{color:var(--md-default-fg-color--light);opacity:0;transform:scale(.75);transition:transform .15s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-search__options>.md-icon:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>.md-icon{opacity:1;pointer-events:auto;transform:scale(1)}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>.md-icon:hover{opacity:.7}[dir=ltr] .md-search__suggest{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__suggest{padding-left:2.2rem;padding-right:3.6rem}.md-search__suggest{align-items:center;color:var(--md-default-fg-color--lighter);display:flex;font-size:.9rem;height:100%;opacity:0;position:absolute;top:0;transition:opacity 50ms;white-space:nowrap;width:100%}@media screen and (min-width:60em){[dir=ltr] .md-search__suggest{padding-left:2.2rem}[dir=rtl] .md-search__suggest{padding-right:2.2rem}.md-search__suggest{font-size:.8rem}}[data-md-toggle=search]:checked~.md-header .md-search__suggest{opacity:1;transition:opacity .3s .1s}[dir=ltr] .md-search__output{border-bottom-left-radius:.1rem}[dir=ltr] .md-search__output,[dir=rtl] .md-search__output{border-bottom-right-radius:.1rem}[dir=rtl] .md-search__output{border-bottom-left-radius:.1rem}.md-search__output{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:var(--md-shadow-z3);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) #0000;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) #0000}.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){[dir=ltr] .md-search-result__meta{padding-left:2.2rem}[dir=rtl] .md-search-result__meta{padding-right:2.2rem}}.md-search-result__list{list-style:none;margin:0;padding:0;-webkit-user-select:none;user-select:none}.md-search-result__item{box-shadow:0 -.05rem 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{cursor:pointer;display:block;outline:none;position:sticky;scroll-snap-align:start;top:0;z-index:1}.md-search-result__more>summary::marker{display:none}.md-search-result__more>summary::-webkit-details-marker{display:none}.md-search-result__more>summary>div{color:var(--md-typeset-a-color);font-size:.64rem;padding:.75em .8rem;transition:color .25s,background-color .25s}@media screen and (min-width:60em){[dir=ltr] .md-search-result__more>summary>div{padding-left:2.2rem}[dir=rtl] .md-search-result__more>summary>div{padding-right:2.2rem}}.md-search-result__more>summary:focus>div,.md-search-result__more>summary:hover>div{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more[open]>summary{background-color:var(--md-default-bg-color)}.md-search-result__article{overflow:hidden;padding:0 .8rem;position:relative}@media screen and (min-width:60em){[dir=ltr] .md-search-result__article{padding-left:2.2rem}[dir=rtl] .md-search-result__article{padding-right:2.2rem}}[dir=ltr] .md-search-result__icon{left:0}[dir=rtl] .md-search-result__icon{right:0}.md-search-result__icon{color:var(--md-default-fg-color--light);height:1.2rem;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-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-search-result__icon:after{transform:scaleX(-1)}.md-search-result .md-typeset{color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.6}.md-search-result .md-typeset h1{color:var(--md-default-fg-color);font-size:.8rem;font-weight:400;line-height:1.4;margin:.55rem 0}.md-search-result .md-typeset h1 mark{text-decoration:none}.md-search-result .md-typeset h2{color:var(--md-default-fg-color);font-size:.64rem;font-weight:700;line-height:1.6;margin:.5em 0}.md-search-result .md-typeset h2 mark{text-decoration:none}.md-search-result__terms{color:var(--md-default-fg-color);display:block;font-size:.64rem;font-style:italic;margin:.5em 0}.md-search-result mark{background-color:initial;color:var(--md-accent-fg-color);text-decoration:underline}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);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 #0000;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid #0000;border-right:.2rem solid #0000;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}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.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:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.1875em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{background-color:var(--md-default-bg-color);display:block;height:100%;position:fixed;top:0;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);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;scroll-snap-type:none;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}.md-header--lifted~.md-container .md-sidebar{top:4.8rem}}.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{scrollbar-gutter:stable;-webkit-backface-visibility:hidden;backface-visibility:hidden;margin:0 .2rem;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap:focus-within,.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb:hover,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@supports selector(::-webkit-scrollbar){.md-sidebar__scrollwrap{scrollbar-gutter:auto}[dir=ltr] .md-sidebar__inner{padding-right:calc(100% - 11.5rem)}[dir=rtl] .md-sidebar__inner{padding-left:calc(100% - 11.5rem)}}@media screen and (max-width:76.1875em){.md-overlay{background-color:#0000008a;height:0;opacity:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@keyframes facts{0%{height:0}to{height:.65rem}}@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}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{display:flex;font-size:.55rem;gap:.4rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0;width:100%}.md-source__repository--active .md-source__facts{animation:facts .25s ease-in}.md-source__fact{overflow:hidden;text-overflow:ellipsis}.md-source__repository--active .md-source__fact{animation:fact .4s ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-position:center;mask-position:center;-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){flex-shrink:0}.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)}:root{--md-status:url('data:image/svg+xml;charset=utf-8,');--md-status--new:url('data:image/svg+xml;charset=utf-8,');--md-status--deprecated:url('data:image/svg+xml;charset=utf-8,');--md-status--encrypted:url('data:image/svg+xml;charset=utf-8,')}.md-status{margin-left:.2rem}.md-status:after{background-color:var(--md-default-fg-color--light);content:"";display:inline-block;height:1.125em;-webkit-mask-image:var(--md-status);mask-image:var(--md-status);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-bottom;width:1.125em}.md-status:hover:after{background-color:currentcolor}.md-status--new:after{-webkit-mask-image:var(--md-status--new);mask-image:var(--md-status--new)}.md-status--deprecated:after{-webkit-mask-image:var(--md-status--deprecated);mask-image:var(--md-status--deprecated)}.md-status--encrypted:after{-webkit-mask-image:var(--md-status--encrypted);mask-image:var(--md-status--encrypted)}.md-tabs{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);display:block;line-height:1.3;overflow:auto;width:100%;z-index:3}@media print{.md-tabs{display:none}}@media screen and (max-width:76.1875em){.md-tabs{display:none}}.md-tabs[hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.2rem}[dir=rtl] .md-tabs__list{margin-right:.2rem}.md-tabs__list{contain:content;display:flex;list-style:none;margin:0;overflow:auto;padding:0;scrollbar-width:none;white-space:nowrap}.md-tabs__list::-webkit-scrollbar{display:none}.md-tabs__item{height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__item--active .md-tabs__link{color:inherit;opacity:1}.md-tabs__link{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:flex;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:focus,.md-tabs__link:hover{color:inherit;opacity:1}[dir=ltr] .md-tabs__link svg{margin-right:.4rem}[dir=rtl] .md-tabs__link svg{margin-left:.4rem}.md-tabs__link svg{fill:currentcolor;height:1.3em}.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[hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}:root{--md-tag-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .md-tags{margin-bottom:.75em;margin-top:-.125em}[dir=ltr] .md-typeset .md-tag{margin-right:.5em}[dir=rtl] .md-typeset .md-tag{margin-left:.5em}.md-typeset .md-tag{background:var(--md-default-fg-color--lightest);border-radius:2.4rem;display:inline-block;font-size:.64rem;font-weight:700;letter-spacing:normal;line-height:1.6;margin-bottom:.5em;padding:.3125em .9375em;vertical-align:middle}.md-typeset .md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-typeset .md-tag[href]:focus,.md-typeset .md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-typeset .md-tag{vertical-align:text-top}.md-typeset .md-tag-icon:before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline-block;height:1.2em;margin-right:.4em;-webkit-mask-image:var(--md-tag-icon);mask-image:var(--md-tag-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset .md-tag-icon[href]:focus:before,.md-typeset .md-tag-icon[href]:hover:before{background-color:var(--md-accent-bg-color)}@keyframes pulse{0%{transform:scale(.95)}75%{transform:scale(1)}to{transform:scale(.95)}}:root{--md-annotation-bg-icon:url('data:image/svg+xml;charset=utf-8,');--md-annotation-icon:url('data:image/svg+xml;charset=utf-8,');--md-tooltip-width:20rem}.md-tooltip{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);font-family:var(--md-text-font-family);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x),100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem);max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:var(--md-tooltip-y);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}.md-tooltip--active{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,z-index 0ms;z-index:2}.focus-visible>.md-tooltip,.md-tooltip:target{outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{font-weight:400;outline:none;vertical-align:text-bottom;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}code .md-annotation{font-family:var(--md-code-font-family);font-size:inherit}.md-annotation:not([hidden]){display:inline-block;line-height:1.25}.md-annotation__index{border-radius:.01px;cursor:pointer;display:inline-block;margin-left:.4ch;margin-right:.4ch;outline:none;overflow:hidden;position:relative;-webkit-user-select:none;user-select:none;vertical-align:text-top;z-index:0}.md-annotation .md-annotation__index{transition:z-index .25s}@media screen{.md-annotation__index{width:2.2ch}[data-md-visible]>.md-annotation__index{animation:pulse 2s infinite}.md-annotation__index:before{background:var(--md-default-bg-color);-webkit-mask-image:var(--md-annotation-bg-icon);mask-image:var(--md-annotation-bg-icon)}.md-annotation__index:after,.md-annotation__index:before{content:"";height:2.2ch;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:-.1ch;width:2.2ch;z-index:-1}.md-annotation__index:after{background-color:var(--md-default-fg-color--lighter);-webkit-mask-image:var(--md-annotation-icon);mask-image:var(--md-annotation-icon);transform:scale(1.0001);transition:background-color .25s,transform .25s}.md-tooltip--active+.md-annotation__index:after{transform:rotate(45deg)}.md-tooltip--active+.md-annotation__index:after,:hover>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}}.md-tooltip--active+.md-annotation__index{animation-play-state:paused;transition-duration:0ms;z-index:2}.md-annotation__index [data-md-annotation-id]{display:inline-block}@media print{.md-annotation__index [data-md-annotation-id]{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);font-weight:700;padding:0 .6ch;white-space:nowrap}.md-annotation__index [data-md-annotation-id]:after{content:attr(data-md-annotation-id)}}.md-typeset .md-annotation-list{counter-reset:xxx;list-style:none}.md-typeset .md-annotation-list li{position:relative}[dir=ltr] .md-typeset .md-annotation-list li:before{left:-2.125em}[dir=rtl] .md-typeset .md-annotation-list li:before{right:-2.125em}.md-typeset .md-annotation-list li:before{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);content:counter(xxx);counter-increment:xxx;font-size:.8875em;font-weight:700;height:2ch;line-height:1.25;min-width:2ch;padding:0 .6ch;position:absolute;text-align:center;top:.25em}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{background-color:var(--md-default-bg-color);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:block;font-size:.7rem;outline:none;padding:.4rem .8rem;position:fixed;top:3.2rem;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[hidden]{transform:translate(50%,.2rem)}.md-top:focus,.md-top:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;vertical-align:-.5em}@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}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.4rem}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.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 (hover:none),(pointer:coarse){.md-version:hover .md-version__list{animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.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:.05rem solid #448aff;border-radius:.2rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid;transition:box-shadow 125ms}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}.md-typeset .admonition:focus-within,.md-typeset details:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .admonition>*,.md-typeset details>*{box-sizing:border-box}.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}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{padding-left:2rem;padding-right:.6rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{padding-left:.6rem;padding-right:2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-left-width:.2rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-right-width:.2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset .admonition-title,.md-typeset summary{background-color:#448aff1a;border:none;font-weight:700;margin:0 -.6rem;padding-bottom:.4rem;padding-top:.4rem;position:relative}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}[dir=ltr] .md-typeset .admonition-title:before,[dir=ltr] .md-typeset summary:before{left:.6rem}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{right:.6rem}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;width:1rem}.md-typeset .admonition-title code,.md-typeset summary code{box-shadow:0 0 0 .05rem var(--md-default-fg-color--lightest)}.md-typeset .admonition.note,.md-typeset details.note{border-color:#448aff}.md-typeset .admonition.note:focus-within,.md-typeset details.note:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .note>.admonition-title,.md-typeset .note>summary{background-color:#448aff1a}.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)}.md-typeset .note>.admonition-title:after,.md-typeset .note>summary:after{color:#448aff}.md-typeset .admonition.abstract,.md-typeset details.abstract{border-color:#00b0ff}.md-typeset .admonition.abstract:focus-within,.md-typeset details.abstract:focus-within{box-shadow:0 0 0 .2rem #00b0ff1a}.md-typeset .abstract>.admonition-title,.md-typeset .abstract>summary{background-color:#00b0ff1a}.md-typeset .abstract>.admonition-title:before,.md-typeset .abstract>summary:before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset .abstract>.admonition-title:after,.md-typeset .abstract>summary:after{color:#00b0ff}.md-typeset .admonition.info,.md-typeset details.info{border-color:#00b8d4}.md-typeset .admonition.info:focus-within,.md-typeset details.info:focus-within{box-shadow:0 0 0 .2rem #00b8d41a}.md-typeset .info>.admonition-title,.md-typeset .info>summary{background-color:#00b8d41a}.md-typeset .info>.admonition-title:before,.md-typeset .info>summary:before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset .info>.admonition-title:after,.md-typeset .info>summary:after{color:#00b8d4}.md-typeset .admonition.tip,.md-typeset details.tip{border-color:#00bfa5}.md-typeset .admonition.tip:focus-within,.md-typeset details.tip:focus-within{box-shadow:0 0 0 .2rem #00bfa51a}.md-typeset .tip>.admonition-title,.md-typeset .tip>summary{background-color:#00bfa51a}.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)}.md-typeset .tip>.admonition-title:after,.md-typeset .tip>summary:after{color:#00bfa5}.md-typeset .admonition.success,.md-typeset details.success{border-color:#00c853}.md-typeset .admonition.success:focus-within,.md-typeset details.success:focus-within{box-shadow:0 0 0 .2rem #00c8531a}.md-typeset .success>.admonition-title,.md-typeset .success>summary{background-color:#00c8531a}.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)}.md-typeset .success>.admonition-title:after,.md-typeset .success>summary:after{color:#00c853}.md-typeset .admonition.question,.md-typeset details.question{border-color:#64dd17}.md-typeset .admonition.question:focus-within,.md-typeset details.question:focus-within{box-shadow:0 0 0 .2rem #64dd171a}.md-typeset .question>.admonition-title,.md-typeset .question>summary{background-color:#64dd171a}.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)}.md-typeset .question>.admonition-title:after,.md-typeset .question>summary:after{color:#64dd17}.md-typeset .admonition.warning,.md-typeset details.warning{border-color:#ff9100}.md-typeset .admonition.warning:focus-within,.md-typeset details.warning:focus-within{box-shadow:0 0 0 .2rem #ff91001a}.md-typeset .warning>.admonition-title,.md-typeset .warning>summary{background-color:#ff91001a}.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)}.md-typeset .warning>.admonition-title:after,.md-typeset .warning>summary:after{color:#ff9100}.md-typeset .admonition.failure,.md-typeset details.failure{border-color:#ff5252}.md-typeset .admonition.failure:focus-within,.md-typeset details.failure:focus-within{box-shadow:0 0 0 .2rem #ff52521a}.md-typeset .failure>.admonition-title,.md-typeset .failure>summary{background-color:#ff52521a}.md-typeset .failure>.admonition-title:before,.md-typeset .failure>summary:before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset .failure>.admonition-title:after,.md-typeset .failure>summary:after{color:#ff5252}.md-typeset .admonition.danger,.md-typeset details.danger{border-color:#ff1744}.md-typeset .admonition.danger:focus-within,.md-typeset details.danger:focus-within{box-shadow:0 0 0 .2rem #ff17441a}.md-typeset .danger>.admonition-title,.md-typeset .danger>summary{background-color:#ff17441a}.md-typeset .danger>.admonition-title:before,.md-typeset .danger>summary:before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset .danger>.admonition-title:after,.md-typeset .danger>summary:after{color:#ff1744}.md-typeset .admonition.bug,.md-typeset details.bug{border-color:#f50057}.md-typeset .admonition.bug:focus-within,.md-typeset details.bug:focus-within{box-shadow:0 0 0 .2rem #f500571a}.md-typeset .bug>.admonition-title,.md-typeset .bug>summary{background-color:#f500571a}.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)}.md-typeset .bug>.admonition-title:after,.md-typeset .bug>summary:after{color:#f50057}.md-typeset .admonition.example,.md-typeset details.example{border-color:#7c4dff}.md-typeset .admonition.example:focus-within,.md-typeset details.example:focus-within{box-shadow:0 0 0 .2rem #7c4dff1a}.md-typeset .example>.admonition-title,.md-typeset .example>summary{background-color:#7c4dff1a}.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)}.md-typeset .example>.admonition-title:after,.md-typeset .example>summary:after{color:#7c4dff}.md-typeset .admonition.quote,.md-typeset details.quote{border-color:#9e9e9e}.md-typeset .admonition.quote:focus-within,.md-typeset details.quote:focus-within{box-shadow:0 0 0 .2rem #9e9e9e1a}.md-typeset .quote>.admonition-title,.md-typeset .quote>summary{background-color:#9e9e9e1a}.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)}.md-typeset .quote>.admonition-title:after,.md-typeset .quote>summary:after{color:#9e9e9e}: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}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right: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:focus-within .footnote-backref{opacity:1;transform:translateX(0);transition:none}.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 [id^="fnref:"]:target>.footnote-ref{outline:auto}.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-position:center;mask-position:center;-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)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.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{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset h1:target,.md-typeset h2:target,.md-typeset h3:target{--md-scroll-offset:0.2rem}.md-typeset h4:target{--md-scroll-offset:0.15rem}.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-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto;width:-webkit-min-content;width:min-content}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset del.critic,.md-typeset ins.critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{-webkit-box-decoration-break:clone;box-decoration-break:clone;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}[dir=ltr] .md-typeset summary{padding-right:1.8rem}[dir=rtl] .md-typeset summary{padding-left:1.8rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:.4rem}[dir=rtl] .md-typeset summary:after{left:.4rem}.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-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-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 span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:flow-root;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.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:sticky;-webkit-user-select:none;user-select:none;z-index:3}.highlight code a[id]{position:absolute;visibility:hidden}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable tbody,.highlighttable td{display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable th.filename span.filename{margin-top:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-top-left-radius:.1rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-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;min-width:0}.linenodiv a{color:inherit}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable>tbody>tr>.code>div>pre>code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset .highlight+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset .highlight+.result:after{clear:both;content:"";display:block}@media screen and (max-width:44.9375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight>.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.code>div>pre>code,.md-content__inner>.highlight>.highlighttable>tbody>tr>.filename span.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.linenos,.md-content__inner>.highlight>pre>code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.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,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-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}:root{--md-tabbed-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-tabbed-icon--next:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .tabbed-set{border-radius:.1rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-accent-fg-color);bottom:0;content:"";display:block;height:2px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid #0000;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.64rem;font-weight:700;padding:.78125em 1.25em .625em;scroll-margin-inline-start:1rem;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-accent-fg-color)}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre,.md-typeset .tabbed-block>pre:first-child{margin:0}.md-typeset .tabbed-block>.highlight:first-child>pre>code,.md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child>.filename{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable{margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.filename span.filename,.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.linenos{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.code>div>pre>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child+.result{margin-top:-.125em}.md-typeset .tabbed-block>.tabbed-set{margin:0}.md-typeset .tabbed-button{align-self:center;border-radius:100%;color:var(--md-default-fg-color--light);cursor:pointer;display:block;height:.9rem;margin-top:.1rem;pointer-events:auto;transition:background-color .25s;width:.9rem}.md-typeset .tabbed-button:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset .tabbed-button:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-tabbed-icon--prev);mask-image:var(--md-tabbed-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color .25s,transform .25s;width:100%}.md-typeset .tabbed-control{background:linear-gradient(to right,var(--md-default-bg-color) 60%,#0000);display:flex;height:1.9rem;justify-content:start;pointer-events:none;position:absolute;transition:opacity 125ms;width:1.2rem}[dir=rtl] .md-typeset .tabbed-control{transform:rotate(180deg)}.md-typeset .tabbed-control[hidden]{opacity:0}.md-typeset .tabbed-control--next{background:linear-gradient(to left,var(--md-default-bg-color) 60%,#0000);justify-content:end;right:0}.md-typeset .tabbed-control--next .tabbed-button:after{-webkit-mask-image:var(--md-tabbed-icon--next);mask-image:var(--md-tabbed-icon--next)}@media screen and (max-width:44.9375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-right:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-left:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-right:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{width:2rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-left:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-right:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-left:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{width:2rem}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-accent-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){background-color:var(--md-accent-fg-color--transparent)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}: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}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lightest);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.15em;width:1.25em}.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)}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color);--md-mermaid-sequence-actor-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actor-fg-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-actor-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-actor-line-color:var(--md-default-fg-color--lighter);--md-mermaid-sequence-actorman-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actorman-line-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-box-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-box-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-label-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-label-fg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-loop-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-loop-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-loop-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-message-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-message-line-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-note-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-border-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-number-bg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-number-fg-color:var(--md-accent-bg-color)}.mermaid{line-height:normal;margin:1em 0}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{float:left}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=ltr] .md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}} \ No newline at end of file diff --git a/v13.1/assets/stylesheets/palette.85d0ee34.min.css b/v13.1/assets/stylesheets/palette.85d0ee34.min.css new file mode 100644 index 00000000..ded5445b --- /dev/null +++ b/v13.1/assets/stylesheets/palette.85d0ee34.min.css @@ -0,0 +1 @@ +@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:#4287ff26;--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:#4287ff4d;--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-typeset-table-color:hsla(var(--md-hue),75%,95%,0.12);--md-typeset-table-color--light:hsla(var(--md-hue),75%,95%,0.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-footer-bg-color:hsla(var(--md-hue),15%,12%,0.87);--md-footer-bg-color--dark:hsla(var(--md-hue),15%,10%,1);--md-shadow-z1:0 0.2rem 0.5rem #0003,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #0000004d,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0006,0 0 0.05rem #00000059;color-scheme:dark}[data-md-color-scheme=slate] img[src$="#gh-light-mode-only"],[data-md-color-scheme=slate] img[src$="#only-light"]{display:none}[data-md-color-scheme=slate][data-md-color-primary=pink]{--md-typeset-a-color:#ed5487}[data-md-color-scheme=slate][data-md-color-primary=purple]{--md-typeset-a-color:#bd78c9}[data-md-color-scheme=slate][data-md-color-primary=deep-purple]{--md-typeset-a-color:#a682e3}[data-md-color-scheme=slate][data-md-color-primary=indigo]{--md-typeset-a-color:#739ae2}[data-md-color-scheme=slate][data-md-color-primary=teal]{--md-typeset-a-color:#00ccb8}[data-md-color-scheme=slate][data-md-color-primary=green]{--md-typeset-a-color:#71c174}[data-md-color-scheme=slate][data-md-color-primary=deep-orange]{--md-typeset-a-color:#ff9575}[data-md-color-scheme=slate][data-md-color-primary=brown]{--md-typeset-a-color:#c7846b}[data-md-color-scheme=slate][data-md-color-primary=black],[data-md-color-scheme=slate][data-md-color-primary=blue-grey],[data-md-color-scheme=slate][data-md-color-primary=grey],[data-md-color-scheme=slate][data-md-color-primary=white]{--md-typeset-a-color:#6c91d5}[data-md-color-switching] *,[data-md-color-switching] :after,[data-md-color-switching] :before{transition-duration:0ms!important}}[data-md-color-accent=red]{--md-accent-fg-color:#ff1947;--md-accent-fg-color--transparent:#ff19471a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=pink]{--md-accent-fg-color:#f50056;--md-accent-fg-color--transparent:#f500561a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=purple]{--md-accent-fg-color:#df41fb;--md-accent-fg-color--transparent:#df41fb1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=deep-purple]{--md-accent-fg-color:#7c4dff;--md-accent-fg-color--transparent:#7c4dff1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=indigo]{--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=blue]{--md-accent-fg-color:#4287ff;--md-accent-fg-color--transparent:#4287ff1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=light-blue]{--md-accent-fg-color:#0091eb;--md-accent-fg-color--transparent:#0091eb1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=cyan]{--md-accent-fg-color:#00bad6;--md-accent-fg-color--transparent:#00bad61a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=teal]{--md-accent-fg-color:#00bda4;--md-accent-fg-color--transparent:#00bda41a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=green]{--md-accent-fg-color:#00c753;--md-accent-fg-color--transparent:#00c7531a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=light-green]{--md-accent-fg-color:#63de17;--md-accent-fg-color--transparent:#63de171a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=lime]{--md-accent-fg-color:#b0eb00;--md-accent-fg-color--transparent:#b0eb001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=yellow]{--md-accent-fg-color:#ffd500;--md-accent-fg-color--transparent:#ffd5001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=amber]{--md-accent-fg-color:#fa0;--md-accent-fg-color--transparent:#ffaa001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=orange]{--md-accent-fg-color:#ff9100;--md-accent-fg-color--transparent:#ff91001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=deep-orange]{--md-accent-fg-color:#ff6e42;--md-accent-fg-color--transparent:#ff6e421a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3}[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:#000000de;--md-primary-bg-color--light:#0000008a}[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:#000000de;--md-primary-bg-color--light:#0000008a}[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:#000000de;--md-primary-bg-color--light:#0000008a}[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:#000000de;--md-primary-bg-color--light:#0000008a}[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:#ffffffb3}[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:#ffffffb3}[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:#ffffffb3;--md-typeset-a-color:#4051b5}[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:#ffffffb3;--md-typeset-a-color:#4051b5}[data-md-color-primary=light-green]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#72ad2e}[data-md-color-primary=lime]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#8b990a}[data-md-color-primary=yellow]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#b8a500}[data-md-color-primary=amber]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#d19d00}[data-md-color-primary=orange]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#e68a00}[data-md-color-primary=white]{--md-primary-fg-color:#fff;--md-primary-fg-color--light:#ffffffb3;--md-primary-fg-color--dark:#00000012;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a;--md-typeset-a-color:#4051b5}[data-md-color-primary=white] .md-button{color:var(--md-typeset-a-color)}[data-md-color-primary=white] .md-button--primary{background-color:var(--md-typeset-a-color);border-color:var(--md-typeset-a-color);color:#fff}@media screen and (min-width:60em){[data-md-color-primary=white] .md-search__form{background-color:#00000012}[data-md-color-primary=white] .md-search__form:hover{background-color:#00000052}[data-md-color-primary=white] .md-search__input+.md-search__icon{color:#000000de}}@media screen and (min-width:76.25em){[data-md-color-primary=white] .md-tabs{border-bottom:.05rem solid #00000012}}[data-md-color-primary=black]{--md-primary-fg-color:#000;--md-primary-fg-color--light:#0000008a;--md-primary-fg-color--dark:#000;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-typeset-a-color:#4051b5}[data-md-color-primary=black] .md-button{color:var(--md-typeset-a-color)}[data-md-color-primary=black] .md-button--primary{background-color:var(--md-typeset-a-color);border-color:var(--md-typeset-a-color);color:#fff}[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:#000000de}}@media screen and (min-width:60em){[data-md-color-primary=black] .md-search__form{background-color:#ffffff1f}[data-md-color-primary=black] .md-search__form:hover{background-color:#ffffff4d}}@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}} \ No newline at end of file diff --git a/v13.1/config/advanced/auth-ldap/index.html b/v13.1/config/advanced/auth-ldap/index.html new file mode 100644 index 00000000..221205fe --- /dev/null +++ b/v13.1/config/advanced/auth-ldap/index.html @@ -0,0 +1,2380 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Advanced | LDAP Authentication - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

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

LDAP Authentication

+ +

Introduction

+

Getting started with ldap and DMS 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.

+
- LDAP_START_TLS=yes
+- ACCOUNT_PROVISIONER=LDAP
+- 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 separately 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
+
+

Active Directory Configurations (Tested with Samba4 AD Implementation)

+

In addition to LDAP explanation above, when Docker Mailserver is intended to be used with Active Directory (or the equivalent implementations like Samba4 AD DC) the following points should be taken into consideration:

+
    +
  • Samba4 Active Directory requires a secure connection to the domain controller (DC), either via SSL/TLS (LDAPS) or via StartTLS.
  • +
  • The username equivalent in Active Directory is: sAMAccountName.
  • +
  • proxyAddresses can be used to store email aliases of single users. The convention is to prefix the email aliases with smtp: (e.g: smtp:some.name@example.com).
  • +
  • Active Directory is used typically not only as LDAP Directory storage, but also as a domain controller, i.e., it will do many things including authenticating users. Mixing Linux and Windows clients requires the usage of RFC2307 attributes, namely uidNumber, gidNumber instead of the typical uid. Assigning different owner to email folders can also be done in this approach, nevertheless there is a bug at the moment in Docker Mailserver that overwrites all permissions when starting the container. Either a manual fix is necessary now, or a temporary workaround to use a hard-coded ldap:uidNumber that equals to 5000 until this issue is fixed.
  • +
  • To deliver the emails to different members of Active Directory Security Group or Distribution Group (similar to mailing lists), use a user-patches.sh script to modify ldap-groups.cf so that it includes leaf_result_attribute = mail and special_result_attribute = member. This can be achieved simply by:
  • +
+

The configuration shown to get the Group to work is from here and here.

+
# user-patches.sh
+
+...
+grep -q '^leaf_result_attribute = mail$' /etc/postfix/ldap-groups.cf || echo "leaf_result_attribute = mail" >> /etc/postfix/ldap-groups.cf
+grep -q '^special_result_attribute = member$' /etc/postfix/ldap-groups.cf || echo "special_result_attribute = member" >> /etc/postfix/ldap-groups.cf
+...
+
+
    +
  • In /etc/ldap/ldap.conf, if the TLS_REQCERT is demand / hard (default), the CA certificate used to verify the LDAP server certificate must be recognized as a trusted CA. This can be done by volume mounting the ca.crt file and updating the trust store via a user-patches.sh script:
  • +
+
# user-patches.sh
+
+...
+cp /MOUNTED_FOLDER/ca.crt /usr/local/share/ca-certificates/
+update-ca-certificates
+...
+
+

The changes on the configurations necessary to work with Active Directory (only changes are listed, the rest of the LDAP configuration can be taken from the other examples shown in this documentation):

+
# If StartTLS is the chosen method to establish a secure connection with Active Directory.
+- LDAP_START_TLS=yes
+- SASLAUTHD_LDAP_START_TLS=yes
+- DOVECOT_TLS=yes
+
+- LDAP_QUERY_FILTER_USER=(&(objectclass=person)(mail=%s))
+- LDAP_QUERY_FILTER_ALIAS=(&(objectclass=person)(proxyAddresses=smtp:%s))
+# Filters Active Directory groups (mail lists). Additional changes on ldap-groups.cf are also required as shown above.
+- LDAP_QUERY_FILTER_GROUP=(&(objectClass=group)(mail=%s))
+- LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s)
+# Allows only Domain admins to send any sender email address, otherwise the sender address must match the LDAP attribute `mail`.
+- SPOOF_PROTECTION=1
+- LDAP_QUERY_FILTER_SENDERS=(|(mail=%s)(proxyAddresses=smtp:%s)(memberOf=cn=Domain Admins,cn=Users,dc=*))
+
+- DOVECOT_USER_FILTER=(&(objectclass=person)(sAMAccountName=%n))
+# At the moment to be able to use %{ldap:uidNumber}, a manual bug fix as described above must be used. Otherwise %{ldap:uidNumber} %{ldap:uidNumber} must be replaced by the hard-coded value 5000.
+- DOVECOT_USER_ATTRS==uid=%{ldap:uidNumber},=gid=5000,=home=/var/mail/%Ln,=mail=maildir:~/Maildir
+- DOVECOT_PASS_ATTRS=sAMAccountName=user,userPassword=password
+- SASLAUTHD_LDAP_FILTER=(&(sAMAccountName=%U)(objectClass=person))
+
+

LDAP Setup Examples

+
+Basic Setup +
services:
+  mailserver:
+    image: ghcr.io/docker-mailserver/docker-mailserver:latest
+    container_name: mailserver
+    hostname: mail.example.com
+
+    ports:
+      - "25:25"
+      - "143:143"
+      - "587:587"
+      - "993:993"
+
+    volumes:
+      - ./docker-data/dms/mail-data/:/var/mail/
+      - ./docker-data/dms/mail-state/:/var/mail-state/
+      - ./docker-data/dms/mail-logs/:/var/log/mail/
+      - ./docker-data/dms/config/:/tmp/docker-mailserver/
+      - /etc/localtime:/etc/localtime:ro
+
+    environment:
+      - ENABLE_SPAMASSASSIN=1
+      - ENABLE_CLAMAV=1
+      - ENABLE_FAIL2BAN=1
+      - ENABLE_POSTGREY=1
+
+      # >>> Postfix LDAP Integration
+      - ACCOUNT_PROVISIONER=LDAP
+      - 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
+
+      - SSL_TYPE=letsencrypt
+      - PERMIT_DOCKER=host
+
+    cap_add:
+      - NET_ADMIN
+
+
+
+Kopano / Zarafa +
services:
+  mailserver:
+    image: ghcr.io/docker-mailserver/docker-mailserver:latest
+    container_name: mailserver
+    hostname: mail.example.com
+
+    ports:
+      - "25:25"
+      - "143:143"
+      - "587:587"
+      - "993:993"
+
+    volumes:
+      - ./docker-data/dms/mail-data/:/var/mail/
+      - ./docker-data/dms/mail-state/:/var/mail-state/
+      - ./docker-data/dms/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
+      - ACCOUNT_PROVISIONER=LDAP
+      - 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
+      - POSTFIX_DAGENT=lmtp:kopano:2003
+      # <<< Kopano Integration
+
+      - SSL_TYPE=letsencrypt
+      - PERMIT_DOCKER=host
+
+    cap_add:
+      - NET_ADMIN
+
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/advanced/dovecot-master-accounts/index.html b/v13.1/config/advanced/dovecot-master-accounts/index.html new file mode 100644 index 00000000..636203e8 --- /dev/null +++ b/v13.1/config/advanced/dovecot-master-accounts/index.html @@ -0,0 +1,2026 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Advanced | Dovecot master accounts - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Dovecot Master Accounts

+ +

Introduction

+

A dovecot master account is able to login as any configured user. This is useful for administrative tasks like hot backups.

+

Configuration

+

It is possible to create, update, delete and list dovecot master accounts using setup.sh. See setup.sh help for usage.

+

This feature is presently not supported with LDAP.

+

Logging in

+

Once a master account is configured, it is possible to connect to any users mailbox using this account. Log in over POP3/IMAP using the following credential scheme:

+

Username: <EMAIL ADDRESS>*<MASTER ACCOUNT NAME>

+

Password: <MASTER ACCOUNT PASSWORD>

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/advanced/full-text-search/index.html b/v13.1/config/advanced/full-text-search/index.html new file mode 100644 index 00000000..1a8331b0 --- /dev/null +++ b/v13.1/config/advanced/full-text-search/index.html @@ -0,0 +1,2254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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. Dovecot supports a variety of community supported FTS indexing backends.

+

DMS comes pre-installed with two plugins that can be enabled with a dovecot config file.

+

Please be aware that indexing consumes memory and takes up additional disk space.

+

Xapian

+

The dovecot-fts-xapian plugin makes use of Xapian. Xapian enables embedding an FTS engine without the need for additional backends.

+

The indexes will be stored as a subfolder named xapian-indexes inside your local mail-data folder (/var/mail internally). With the default settings, 10GB of email data may generate around 4GB of indexed data.

+

While indexing is memory intensive, you can configure the plugin to limit the amount of memory consumed by the index workers. With Xapian being small and fast, this plugin is a good choice for low memory environments (2GB) as compared to Solr.

+

Setup

+
    +
  1. +

    To configure fts-xapian as a dovecot plugin, create a file at docker-data/dms/config/dovecot/fts-xapian-plugin.conf and place the following in it:

    +
    mail_plugins = $mail_plugins fts fts_xapian
    +
    +plugin {
    +    fts = xapian
    +    fts_xapian = partial=3 full=20 verbose=0
    +
    +    fts_autoindex = yes
    +    fts_enforced = yes
    +
    +    # disable indexing of folders
    +    # fts_autoindex_exclude = \Trash
    +
    +    # Index attachements
    +    # fts_decoder = decode2text
    +}
    +
    +service indexer-worker {
    +    # limit size of indexer-worker RAM usage, ex: 512MB, 1GB, 2GB
    +    vsz_limit = 1GB
    +}
    +
    +# service decode2text {
    +#     executable = script /usr/libexec/dovecot/decode2text.sh
    +#     user = dovecot
    +#     unix_listener decode2text {
    +#         mode = 0666
    +#     }
    +# }
    +
    +

    adjust the settings to tune for your desired memory limits, exclude folders and enable searching text inside of attachments

    +
  2. +
  3. +

    Update compose.yaml to load the previously created dovecot plugin config file:

    +
      services:
    +    mailserver:
    +      image: ghcr.io/docker-mailserver/docker-mailserver:latest
    +      container_name: mailserver
    +      hostname: mail.example.com
    +      env_file: mailserver.env
    +      ports:
    +        - "25:25"    # SMTP  (explicit TLS => STARTTLS)
    +        - "143:143"  # IMAP4 (explicit TLS => STARTTLS)
    +        - "465:465"  # ESMTP (implicit TLS)
    +        - "587:587"  # ESMTP (explicit TLS => STARTTLS)
    +        - "993:993"  # IMAP4 (implicit TLS)
    +      volumes:
    +        - ./docker-data/dms/mail-data/:/var/mail/
    +        - ./docker-data/dms/mail-state/:/var/mail-state/
    +        - ./docker-data/dms/mail-logs/:/var/log/mail/
    +        - ./docker-data/dms/config/:/tmp/docker-mailserver/
    +        - ./docker-data/dms/config/dovecot/fts-xapian-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro
    +        - /etc/localtime:/etc/localtime:ro
    +      restart: always
    +      stop_grace_period: 1m
    +      cap_add:
    +        - NET_ADMIN
    +
    +
  4. +
  5. +

    Recreate containers:

    +
    docker compose down
    +docker compose up -d
    +
    +
  6. +
  7. +

    Initialize indexing on all users for all mail:

    +
    docker compose exec mailserver doveadm index -A -q \*
    +
    +
  8. +
  9. +

    Run the following command in a daily cron job:

    +

    docker compose exec mailserver doveadm fts optimize -A
    +
    +Or like the Spamassassin example shows, you can instead use cron from within DMS to avoid potential errors if the mail server is not running:

    +
  10. +
+
+Example +

Create a system cron file:

+
# in the compose.yaml root directory
+mkdir -p ./docker-data/dms/cron # if you didn't have this folder before
+touch ./docker-data/dms/cron/fts_xapian
+chown root:root ./docker-data/dms/cron/fts_xapian
+chmod 0644 ./docker-data/dms/cron/fts_xapian
+
+

Edit the system cron file nano ./docker-data/dms/cron/fts_xapian, and set an appropriate configuration:

+
# Adding `MAILTO=""` prevents cron emailing notifications of the task outcome each run
+MAILTO=""
+#
+# m h dom mon dow user command
+#
+# Everyday 4:00AM, optimize index files
+0  4 * * * root  doveadm fts optimize -A
+
+

Then with compose.yaml:

+
services:
+  mailserver:
+    image: ghcr.io/docker-mailserver/docker-mailserver:latest
+    volumes:
+      - ./docker-data/dms/cron/fts_xapian:/etc/cron.d/fts_xapian
+
+
+

Solr

+

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.

+

Solr is a mature and fast indexing backend that runs on the JVM. The indexes are relatively compact compared to the size of your total email.

+

However, Solr also requires a fair bit of RAM. While Solr is highly tuneable, it may require a bit of testing to get it right.

+

Setup

+
    +
  1. +

    compose.yaml:

    +
      solr:
    +    image: lmmdock/dovecot-solr:latest
    +    volumes:
    +      - ./docker-data/dms/config/dovecot/solr-dovecot:/opt/solr/server/solr/dovecot
    +    restart: always
    +
    +  mailserver:
    +    depends_on:
    +      - solr
    +    image: ghcr.io/docker-mailserver/docker-mailserver:latest
    +    ...
    +    volumes:
    +      ...
    +      - ./docker-data/dms/config/dovecot/10-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro
    +    ...
    +
    +
  2. +
  3. +

    ./docker-data/dms/config/dovecot/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/v13.1/config/advanced/ipv6/index.html b/v13.1/config/advanced/ipv6/index.html new file mode 100644 index 00000000..ada0cc1e --- /dev/null +++ b/v13.1/config/advanced/ipv6/index.html @@ -0,0 +1,2253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Advanced | IPv6 - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

IPv6

+ +
+

Ample Opportunities for Issues

+

Numerous bug reports have been raised in the past about IPv6. Please make sure your setup around DMS is correct when using IPv6!

+
+

IPv6 networking problems with Docker defaults

+

What can go wrong?

+

If your host system supports IPv6 and an AAAA DNS record exists to direct IPv6 traffic to DMS, you may experience issues when an IPv6 connection is made:

+
    +
  • The original client IP is replaced with the gateway IP of a docker network.
  • +
  • Connections fail or hang.
  • +
+

The impact of losing the real IP of the client connection can negatively affect DMS:

+
    +
  • Users unable to login (Fail2Ban action triggered by repeated login failures all seen as from the same internal Gateway IP)
  • +
  • Mail inbound to DMS is rejected (SPF verification failure, IP mismatch)
  • +
  • Delivery failures from sender reputation being reduced (due to bouncing inbound mail from rejected IPv6 clients)
  • +
  • Some services may be configured to trust connecting clients within the containers subnet, which includes the Gateway IP. This can risk bypassing or relaxing security measures, such as exposing an open relay.
  • +
+

Why does this happen?

+

When the host network receives a connection to a containers published port, it is routed to the containers internal network managed by Docker (typically a bridge network).

+

By default, the Docker daemon only assigns IPv4 addresses to containers, thus it will only accept IPv4 connections (unless a docker-proxy process is listening, which the default daemon setting userland-proxy: true enables). With the daemon setting userland-proxy: true (default), IPv6 connections from the host can also be accepted and routed to containers (even when they only have IPv4 addresses assigned). userland-proxy: false will require the container to have atleast an IPv6 address assigned.

+

This can be problematic for IPv6 host connections when internally the container is no longer aware of the original client IPv6 address, as it has been proxied through the IPv4 or IPv6 gateway address of it's connected network (eg: 172.17.0.1 - Docker allocates networks from a set of default subnets).

+

This can be fixed by enabling a Docker network to assign IPv6 addresses to containers, along with some additional configuration. Alternatively you could configure the opposite to prevent IPv6 connections being made.

+

Prevent IPv6 connections

+
    +
  • Avoiding an AAAA DNS record for your DMS FQDN would prevent resolving an IPv6 address to connect to.
  • +
  • You can also use userland-proxy: false, which will fail to establish a remote connection to DMS (provided no IPv6 address was assigned).
  • +
+
+

With UFW or Firewalld

+

When one of these firewall frontends are active, remote clients should fail to connect instead of being masqueraded as the docker network gateway IP. Keep in mind that this only affects remote clients, it does not affect local IPv6 connections originating within the same host.

+
+

Enable proper IPv6 support

+

You can enable IPv6 support in Docker for container networks, however compatibility concerns may affect your success.

+

The official Docker documentation on enabling IPv6 has been improving and is a good resource to reference.

+

Enable ip6tables support so that Docker will manage IPv6 networking rules as well. This will allow for IPv6 NAT to work like the existing IPv4 NAT already does for your containers, avoiding the above issue with external connections having their IP address seen as the container network gateway IP (provided an IPv6 address is also assigned to the container).

+
+

Configure the following in /etc/docker/daemon.json

+
{
+  "ip6tables": true,
+  "experimental" : true,
+  "userland-proxy": true
+}
+
+ +

Now restart the daemon if it's running: systemctl restart docker.

+
+

Next, configure a network with an IPv6 subnet for your container with any of these examples:

+
+Create an IPv6 ULA subnet +
+About these examples +

These examples are focused on a IPv6 ULA subnet which is suitable for most users as described in the next section.

+
    +
  • You may prefer a subnet size smaller than /64 (eg: /112, which still provides over 65k IPv6 addresses), especially if instead configuring for an IPv6 GUA subnet.
  • +
  • The network will also implicitly be assigned an IPv4 subnet (from the Docker daemon config default-address-pools).
  • +
+
+
+
+
+

The preferred approach is with user-defined networks via compose.yaml (recommended) or CLI with docker network create:

+
+
+
+

Create the network in compose.yaml and attach a service to it:

+
compose.yaml
services:
+  mailserver:
+    networks:
+      - dms-ipv6
+
+networks:
+  dms-ipv6:
+    enable_ipv6: true
+    ipam:
+      config:
+        - subnet: fd00:cafe:face:feed::/64
+
+
+Override the implicit default network +

You can optionally avoid the service assignment by overriding the default user-defined network that Docker Compose generates. Just replace dms-ipv6 with default.

+

The Docker Compose default bridge is not affected by settings for the default bridge (aka docker0) in /etc/docker/daemon.json.

+
+
+Using the network outside of this compose.yaml +

To reference this network externally (from other compose files or docker run), assign the networks name key in compose.yaml.

+
+
+
+

Create the network via a CLI command (which can then be used with docker run --network dms-ipv6):

+
docker network create --ipv6 --subnet fd00:cafe:face:feed::/64 dms-ipv6
+
+

Optionally reference it from one or more compose.yaml files:

+
compose.yaml
services:
+  mailserver:
+    networks:
+      - dms-ipv6
+
+networks:
+  dms-ipv6:
+    external: true
+
+
+
+
+
+
+
+

This approach is discouraged

+

The bridge network is considered legacy.

+
+

Add these two extra IPv6 settings to your daemon config. They only apply to the default bridge docker network aka docker0 (which containers are attached to by default when using docker run).

+
/etc/docker/daemon.json
{
+  "ipv6": true,
+  "fixed-cidr-v6": "fd00:cafe:face:feed::/64",
+}
+
+

Compose projects can also use this network via network_mode:

+
compose.yaml
services:
+  mailserver:
+    network_mode: bridge
+
+
+
+
+
+
+

Do not use 2001:db8:1::/64 for your private subnet

+

The 2001:db8 address prefix is reserved for documentation. Avoid creating a subnet with this prefix.

+

Presently this is used in examples for Dockers IPv6 docs as a placeholder, while mixed in with private IPv4 addresses which can be misleading.

+
+

Configuring an IPv6 subnet

+

If you've configured IPv6 address pools in /etc/docker/daemon.json, you do not need to specify a subnet explicitly. Otherwise if you're unsure what value to provide, here's a quick guide (Tip: Prefer IPv6 ULA, it's the least hassle):

+
    +
  • fd00:cafe:face:feed::/64 is an example of a IPv6 ULA subnet. ULA addresses are akin to the private IPv4 subnets you may already be familiar with. You can use that example, or choose your own ULA address. This is a good choice for getting Docker containers to their have networks support IPv6 via NAT like they already do by default with IPv4.
  • +
  • IPv6 without NAT, using public address space like your server is assigned belongs to an IPv6 GUA subnet.
      +
    • Typically these will be a /64 block assigned to your host, but this varies by provider.
    • +
    • These addresses do not need to publish ports of a container to another IP to be publicly reached (thus ip6tables: true is not required), you will want a firewall configured to manage which ports are accessible instead as no NAT is involved. Note that this may not be desired if the container should also be reachable via the host IPv4 public address.
    • +
    • You may want to subdivide the /64 into smaller subnets for Docker to use only portions of the /64. This can reduce some routing features, and require additional setup / management via a NDP Proxy for your public interface to know of IPv6 assignments managed by Docker and accept external traffic.
    • +
    +
  • +
+

Verify remote IP is correct

+

With Docker CLI or Docker Compose, run a traefik/whoami container with your IPv6 docker network and port 80 published. You can then send a curl request (or via address in the browser) from another host (as your remote client) with an IPv6 network, the RemoteAddr value returned should match your client IPv6 address.

+
docker run --rm -d --network dms-ipv6 -p 80:80 traefik/whoami
+# On a different host, replace `2001:db8::1` with your DMS host IPv6 address
+curl --max-time 5 http://[2001:db8::1]:80
+
+
+

IPv6 ULA address priority

+

DNS lookups that have records for both IPv4 and IPv6 addresses (eg: localhost) may prefer IPv4 over IPv6 (ULA) for private addresses, whereas for public addresses IPv6 has priority. This shouldn't be anything to worry about, but can come across as a surprise when testing your IPv6 setup on the same host instead of from a remote client.

+

The preference can be controlled with /etc/gai.conf, and appears was configured this way based on the assumption that IPv6 ULA would never be used with NAT. It should only affect the destination resolved for outgoing connections, which for IPv6 ULA should only really affect connections between your containers / host. In future IPv6 ULA may also be prioritized.

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

Kubernetes

+ +

Introduction

+

This article describes how to deploy DMS to Kubernetes. Please note that there is also a Helm chart available.

+
+

Requirements

+

We assume basic knowledge about Kubernetes from the reader. Moreover, we assume the reader to have a basic understanding of mail servers. Ideally, the reader has deployed DMS before in an easier setup with Docker (Compose).

+
+
+

About Support for Kubernetes

+

Please note that Kubernetes is not officially supported and we do not build images specifically designed for it. When opening an issue, please remember that only Docker & Docker Compose are officially supported.

+

This content is entirely community-supported. If you find errors, please open an issue and provide a PR.

+
+

Manifests

+

Configuration

+

We want to provide the basic configuration in the form of environment variables with a ConfigMap. Note that this is just an example configuration; tune the ConfigMap to your needs.

+
---
+apiVersion: v1
+kind: ConfigMap
+
+metadata:
+  name: mailserver.environment
+
+immutable: false
+
+data:
+  TLS_LEVEL: modern
+  POSTSCREEN_ACTION: drop
+  OVERRIDE_HOSTNAME: mail.example.com
+  FAIL2BAN_BLOCKTYPE: drop
+  POSTMASTER_ADDRESS: postmaster@example.com
+  UPDATE_CHECK_INTERVAL: 10d
+  POSTFIX_INET_PROTOCOLS: ipv4
+  ONE_DIR: '1'
+  ENABLE_CLAMAV: '1'
+  ENABLE_POSTGREY: '0'
+  ENABLE_FAIL2BAN: '1'
+  AMAVIS_LOGLEVEL: '-1'
+  SPOOF_PROTECTION: '1'
+  MOVE_SPAM_TO_JUNK: '1'
+  ENABLE_UPDATE_CHECK: '1'
+  ENABLE_SPAMASSASSIN: '1'
+  SUPERVISOR_LOGLEVEL: warn
+  SPAMASSASSIN_SPAM_TO_INBOX: '1'
+
+  # here, we provide an example for the SSL configuration
+  SSL_TYPE: manual
+  SSL_CERT_PATH: /secrets/ssl/rsa/tls.crt
+  SSL_KEY_PATH: /secrets/ssl/rsa/tls.key
+
+

We can also make use of user-provided configuration files, e.g. user-patches.sh, postfix-accounts.cf and more, to adjust DMS to our likings. We encourage you to have a look at Kustomize for creating ConfigMaps from multiple files, but for now, we will provide a simple, hand-written example. This example is absolutely minimal and only goes to show what can be done.

+
---
+apiVersion: v1
+kind: ConfigMap
+
+metadata:
+  name: mailserver.files
+
+data:
+  postfix-accounts.cf: |
+    test@example.com|{SHA512-CRYPT}$6$someHashValueHere
+    other@example.com|{SHA512-CRYPT}$6$someOtherHashValueHere
+
+
+

Static Configuration

+

With the configuration shown above, you can not dynamically add accounts as the configuration file mounted into the mail server can not be written to.

+

Use persistent volumes for production deployments.

+
+

Persistence

+

Thereafter, we need persistence for our data. Make sure you have a storage provisioner and that you choose the correct storageClassName.

+
---
+apiVersion: v1
+kind: PersistentVolumeClaim
+
+metadata:
+  name: data
+
+spec:
+  storageClassName: local-path
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 25Gi
+
+

Service

+

A Service is required for getting the traffic to the pod itself. The service is somewhat crucial. Its configuration determines whether the original IP from the sender will be kept. More about this further down below.

+

The configuration you're seeing does keep the original IP, but you will not be able to scale this way. We have chosen to go this route in this case because we think most Kubernetes users will only want to have one instance.

+
---
+apiVersion: v1
+kind: Service
+
+metadata:
+  name: mailserver
+  labels:
+    app: mailserver
+
+spec:
+  type: LoadBalancer
+
+  selector:
+    app: mailserver
+
+  ports:
+    # Transfer
+    - name: transfer
+      port: 25
+      targetPort: transfer
+      protocol: TCP
+    # ESMTP with implicit TLS
+    - name: esmtp-implicit
+      port: 465
+      targetPort: esmtp-implicit
+      protocol: TCP
+    # ESMTP with explicit TLS (STARTTLS)
+    - name: esmtp-explicit
+      port: 587
+      targetPort: esmtp-explicit
+      protocol: TCP
+    # IMAPS with implicit TLS
+    - name: imap-implicit
+      port: 993
+      targetPort: imap-implicit
+      protocol: TCP
+
+

Deployments

+

Last but not least, the Deployment becomes the most complex component. It instructs Kubernetes how to run the DMS container and how to apply your ConfigMaps, persisted storage, etc. Additionally, we can set options to enforce runtime security here.

+
---
+apiVersion: apps/v1
+kind: Deployment
+
+metadata:
+  name: mailserver
+
+  annotations:
+    ignore-check.kube-linter.io/run-as-non-root: >-
+      'mailserver' needs to run as root
+    ignore-check.kube-linter.io/privileged-ports: >-
+      'mailserver' needs privileged ports
+    ignore-check.kube-linter.io/no-read-only-root-fs: >-
+      There are too many files written to make The
+      root FS read-only
+
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: mailserver
+
+  template:
+    metadata:
+      labels:
+        app: mailserver
+
+      annotations:
+        container.apparmor.security.beta.kubernetes.io/mailserver: runtime/default
+
+    spec:
+      hostname: mail
+      containers:
+        - name: mailserver
+          image: ghcr.io/docker-mailserver/docker-mailserver:latest
+          imagePullPolicy: IfNotPresent
+
+          securityContext:
+            # Required to support SGID via `postdrop` executable
+            # in `/var/mail-state` for Postfix (maildrop + public dirs):
+            # https://github.com/docker-mailserver/docker-mailserver/pull/3625
+            allowPrivilegeEscalation: true
+            readOnlyRootFilesystem: false
+            runAsUser: 0
+            runAsGroup: 0
+            runAsNonRoot: false
+            privileged: false
+            capabilities:
+              add:
+                # file permission capabilities
+                - CHOWN
+                - FOWNER
+                - MKNOD
+                - SETGID
+                - SETUID
+                - DAC_OVERRIDE
+                # network capabilities
+                - NET_ADMIN  # needed for F2B
+                - NET_RAW    # needed for F2B
+                - NET_BIND_SERVICE
+                # miscellaneous  capabilities
+                - SYS_CHROOT
+                - KILL
+              drop: [ALL]
+            seccompProfile:
+              type: RuntimeDefault
+
+          # You want to tune this to your needs. If you disable ClamAV,
+          #   you can use less RAM and CPU. This becomes important in
+          #   case you're low on resources and Kubernetes refuses to
+          #   schedule new pods.
+          resources:
+            limits:
+              memory: 4Gi
+              cpu: 1500m
+            requests:
+              memory: 2Gi
+              cpu: 600m
+
+          volumeMounts:
+            - name: files
+              subPath: postfix-accounts.cf
+              mountPath: /tmp/docker-mailserver/postfix-accounts.cf
+              readOnly: true
+
+            # PVCs
+            - name: data
+              mountPath: /var/mail
+              subPath: data
+              readOnly: false
+            - name: data
+              mountPath: /var/mail-state
+              subPath: state
+              readOnly: false
+            - name: data
+              mountPath: /var/log/mail
+              subPath: log
+              readOnly: false
+
+            # certificates
+            - name: certificates-rsa
+              mountPath: /secrets/ssl/rsa/
+              readOnly: true
+
+            # other
+            - name: tmp-files
+              mountPath: /tmp
+              readOnly: false
+
+          ports:
+            - name: transfer
+              containerPort: 25
+              protocol: TCP
+            - name: esmtp-implicit
+              containerPort: 465
+              protocol: TCP
+            - name: esmtp-explicit
+              containerPort: 587
+            - name: imap-implicit
+              containerPort: 993
+              protocol: TCP
+
+          envFrom:
+            - configMapRef:
+                name: mailserver.environment
+
+      restartPolicy: Always
+
+      volumes:
+        # configuration files
+        - name: files
+          configMap:
+            name: mailserver.files
+
+        # PVCs
+        - name: data
+          persistentVolumeClaim:
+            claimName: data
+
+        # certificates
+        - name: certificates-rsa
+          secret:
+            secretName: mail-tls-certificate-rsa
+            items:
+              - key: tls.key
+                path: tls.key
+              - key: tls.crt
+                path: tls.crt
+
+        # other
+        - name: tmp-files
+          emptyDir: {}
+
+

Certificates - An Example

+

In this example, we use cert-manager to supply RSA certificates. You can also supply RSA certificates as fallback certificates, which DMS supports out of the box with SSL_ALT_CERT_PATH and SSL_ALT_KEY_PATH, and provide ECDSA as the proper certificates.

+
---
+apiVersion: cert-manager.io/v1
+kind: Certificate
+
+metadata:
+  name: mail-tls-certificate-rsa
+
+spec:
+  secretName: mail-tls-certificate-rsa
+  isCA: false
+  privateKey:
+    algorithm: RSA
+    encoding: PKCS1
+    size: 2048
+  dnsNames: [mail.example.com]
+  issuerRef:
+    name: mail-issuer
+    kind: Issuer
+
+
+

Attention

+

You will need to have cert-manager configured. Especially the issue will need to be configured. Since we do not know how you want or need your certificates to be supplied, we do not provide more configuration here. The documentation for cert-manager is excellent.

+
+

Sensitive Data

+
+

Sensitive Data

+

For storing OpenDKIM keys, TLS certificates or any sort of sensitive data, you should be using Secrets. You can mount secrets like ConfigMaps and use them the same way.

+
+

The TLS docs page provides guidance when it comes to certificates and transport layer security. Always provide sensitive information vai Secrets.

+

Exposing your Mail Server to the Outside World

+

The more difficult part with Kubernetes is to expose a deployed DMS to the outside world. Kubernetes provides multiple ways for doing that; each has downsides and complexity. The major problem with exposing DMS to outside world in Kubernetes is to preserve the real client IP. The real client IP is required by DMS for performing IP-based SPF checks and spam checks. If you do not require SPF checks for incoming mails, you may disable them in your Postfix configuration by dropping the line that states: check_policy_service unix:private/policyd-spf.

+

The easiest approach was covered above, using externalTrafficPolicy: Local, which disables the service proxy, but makes the service local as well (which does not scale). This approach only works when you are given the correct (that is, a public and routable) IP address by a load balancer (like MetalLB). In this sense, the approach above is similar to the next example below. We want to provide you with a few alternatives too. But we also want to communicate the idea of another simple method: you could use a load-balancer without an external IP and DNAT the network traffic to the mail server. After all, this does not interfere with SPF checks because it keeps the origin IP address. If no dedicated external IP address is available, you could try the latter approach, if one is available, use the former.

+

External IPs Service

+

The simplest way is to expose DMS as a Service with external IPs. This is very similar to the approach taken above. Here, an external IP is given to the service directly by you. With the approach above, you tell your load-balancer to do this.

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

This approach

+
    +
  • does not preserve the real client IP, so SPF check of incoming mail will fail.
  • +
  • requires you to specify the exposed IPs explicitly.
  • +
+

Proxy port to Service

+

The proxy pod helps to avoid the necessity of specifying external IPs explicitly. This comes at the cost of complexity; you must deploy a proxy pod on each Node you want to expose DMS on.

+

This approach

+
    +
  • does not preserve the real client IP, so SPF check of incoming mail will fail.
  • +
+

Bind to concrete Node and use host network

+

One way to preserve the real client IP is to use hostPort and hostNetwork: true. This comes at the cost of availability; you can reach DMS from the outside world only via IPs of Node where DMS is deployed.

+
---
+apiVersion: extensions/v1beta1
+kind: Deployment
+
+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
+        #  ...
+
+

With this approach,

+
    +
  • it is not possible to access DMS via other cluster Nodes, only via the Node DMS was deployed at.
  • +
  • every Port within the Container is exposed on the Host side.
  • +
+

Proxy Port to Service via PROXY Protocol

+

This way is ideologically the same as using a proxy pod, but instead of a separate proxy pod, you configure your ingress to proxy TCP traffic to the DMS 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"
+
+
+

HAProxy

+

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:

+
+HAProxy 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
+    submissions/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
+
+
+

With this approach,

+
    +
  • it is not possible to access DMS via cluster-DNS, as the PROXY protocol is required for incoming connections.
  • +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/advanced/mail-fetchmail/index.html b/v13.1/config/advanced/mail-fetchmail/index.html new file mode 100644 index 00000000..306d6aa4 --- /dev/null +++ b/v13.1/config/advanced/mail-fetchmail/index.html @@ -0,0 +1,2148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 compose.yaml file should look like following snippet:

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

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

+
├── docker-data/dms/config
+│   ├── dovecot.cf
+│   ├── fetchmail.cf
+│   ├── postfix-accounts.cf
+│   └── postfix-virtual.cf
+├── compose.yaml
+└── 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.gmail.com' proto imap
+  user 'username'
+  pass 'secret'
+  is 'user1@example.com'
+  ssl
+
+
+

POP3 Configuration

+
+

Example

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

Caution

+

Don’t forget the last line! (eg: is 'user1@example.com'). After is, you have to specify an email address from the configuration file: docker-data/dms/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 information 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/v13.1/config/advanced/mail-forwarding/aws-ses/index.html b/v13.1/config/advanced/mail-forwarding/aws-ses/index.html new file mode 100644 index 00000000..03206df9 --- /dev/null +++ b/v13.1/config/advanced/mail-forwarding/aws-ses/index.html @@ -0,0 +1,1951 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mail Forwarding | AWS SES - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

AWS SES

+ +

Amazon SES (Simple Email Service) is intended to provide a simple way for cloud based applications to send email and receive email. For the purposes of this project only sending email via SES is supported. Older versions of docker-mailserver used AWS_SES_HOST and AWS_SES_USERPASS to configure sending, this has changed and the setup is managed through Configure Relay Hosts.

+

You will need to create some Amazon SES SMTP credentials. The SMTP credentials you create will be used to populate the RELAY_USER and RELAY_PASSWORD environment variables.

+

The RELAY_HOST should match your AWS SES region, the RELAY_PORT will be 587.

+

If all of your email is being forwarded through AWS SES, DEFAULT_RELAY_HOST should be set accordingly.

+

Example: +

DEFAULT_RELAY_HOST=[email-smtp.us-west-2.amazonaws.com]:587
+

+
+

Note

+

If you set up AWS Easy DKIM you can safely skip setting up DKIM as the AWS SES will take care of signing your outgoing email.

+
+

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/v13.1/config/advanced/mail-forwarding/relay-hosts/index.html b/v13.1/config/advanced/mail-forwarding/relay-hosts/index.html new file mode 100644 index 00000000..85596ffe --- /dev/null +++ b/v13.1/config/advanced/mail-forwarding/relay-hosts/index.html @@ -0,0 +1,2124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 (aka '', or no ENV set) 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 docker-data/dms/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 through 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 docker-data/dms/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 docker-data/dms/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 docker-data/dms/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.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/advanced/mail-getmail/index.html b/v13.1/config/advanced/mail-getmail/index.html new file mode 100644 index 00000000..29f54163 --- /dev/null +++ b/v13.1/config/advanced/mail-getmail/index.html @@ -0,0 +1,2113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Advanced | Email Gathering with Getmail - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Email Gathering with Getmail

+ +

To enable the getmail service to retrieve e-mails set the environment variable ENABLE_GETMAIL to 1. Your compose.yaml file should include the following:

+
environment:
+  - ENABLE_GETMAIL=1
+  - GETMAIL_POLL=5
+
+

In your DMS config volume (eg: docker-data/dms/config/), create a getmail-<ID>.cf file for each remote account that you want to retrieve mail and store into a local DMS account. <ID> should be replaced by you, and is just the rest of the filename (eg: getmail-example.cf). The contents of each file should be configuration like documented below.

+

The directory structure should similar to this:

+
├── docker-data/dms/config
+│   ├── dovecot.cf
+│   ├── getmail-example.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.

+

Common Options

+

The default options added to each getmail config are:

+
[options]
+verbose = 0
+read_all = false
+delete = false
+max_messages_per_session = 500
+received = false
+delivered_to = false
+
+

If you want to use a different base config, mount a file to /etc/getmailrc_general. This file will replace the default "Common Options" base config above, that all getmail-<ID>.cf files will extend with their configs when used.

+
+IMAP Configuration +

This example will:

+
    +
  1. Connect to the remote IMAP server from Gmail.
  2. +
  3. Retrieve mail from the gmail account alice with password notsecure.
  4. +
  5. Store any mail retrieved from the remote mail-server into DMS for the user1@example.com account that DMS manages.
  6. +
+
[retriever]
+type = SimpleIMAPRetriever
+server = imap.gmail.com
+username = alice
+password = notsecure
+[destination]
+type = MDA_external
+path = /usr/lib/dovecot/deliver
+allow_root_commands = true
+arguments =("-d","user1@example.com")
+
+
+
+POP3 Configuration +

Just like the IMAP example above, but instead via POP3 protocol if you prefer that over IMAP.

+
[retriever]
+type = SimplePOP3Retriever
+server = pop3.gmail.com
+username = alice
+password = notsecure
+[destination]
+type = MDA_external
+path = /usr/lib/dovecot/deliver
+allow_root_commands = true
+arguments =("-d","user1@example.com")
+
+
+

Polling Interval

+

By default the getmail service checks external mail accounts for new mail every 5 minutes. That polling interval is configurable via the GETMAIL_POLL ENV variable, with a value in minutes (default: 5, min: 1, max: 30):

+
environment:
+  - GETMAIL_POLL=1
+
+

XOAUTH2 Authentication

+

It is possible to utilize the getmail-gmail-xoauth-tokens helper to provide authentication using xoauth2 for gmail (example 12) or Microsoft Office 365 (example 13)

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/advanced/mail-sieve/index.html b/v13.1/config/advanced/mail-sieve/index.html new file mode 100644 index 00000000..52ab9edb --- /dev/null +++ b/v13.1/config/advanced/mail-sieve/index.html @@ -0,0 +1,2097 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 docker-data/dms/config/before.dovecot.sieve or a docker-data/dms/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/example.com/user1/home/.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 docker-data/dms/config/ path for each user login that needs a filter. The file name provided should be in the form <user_login>.dovecot.sieve, so for example for user1@example.com you should provide a Sieve file named docker-data/dms/config/user1@example.com.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@not-example.com";
+
+
+

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

+
+

Example

+
redirect "user2@not-example.com";
+
+
+

You can also use external programs to filter or pipe (process) messages by adding executable scripts in docker-data/dms/config/sieve-pipe or docker-data/dms/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.

+

Automatic Sorting Based on Subaddresses

+

It is possible to sort subaddresses such as user+mailing-lists@example.com into a corresponding folder (here: INBOX/Mailing-lists) automatically.

+
require ["envelope", "fileinto", "mailbox", "subaddress", "variables"];
+
+if envelope :detail :matches "to" "*" {
+  set :lower :upperfirst "tag" "${1}";
+  if mailboxexists "INBOX.${1}" {
+    fileinto "INBOX.${1}";
+  } else {
+    fileinto :create "INBOX.${tag}";
+  }
+}
+
+

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

+
# compose.yaml
+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/example.com/user1/home/sieve. Just one Sieve script might be active for a user and is sym-linked to /var/mail/example.com/user1/home/.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 backed up 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/v13.1/config/advanced/maintenance/update-and-cleanup/index.html b/v13.1/config/advanced/maintenance/update-and-cleanup/index.html new file mode 100644 index 00000000..ff54f883 --- /dev/null +++ b/v13.1/config/advanced/maintenance/update-and-cleanup/index.html @@ -0,0 +1,1971 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Maintenance | Update and Cleanup - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Update and Cleanup

+ +

containrrr/watchtower is a service that monitors Docker images for updates, automatically applying them to running containers.

+
+

Automatic image updates + cleanup

+

Run a watchtower container with access to docker.sock, enabling the service to manage Docker:

+
compose.yaml
services:
+  watchtower:
+    image: containrrr/watchtower:latest
+    # Automatic cleanup (removes older image pulls from wasting disk space):
+    environment:
+      - WATCHTOWER_CLEANUP=true
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
+
+
+
+

The image tag used for a container is monitored for updates (eg: :latest, :edge, :13)

+

The automatic update support is only for updates to that specific image tag.

+
    +
  • Your container will not update to a new major version tag (unless using :latest).
  • +
  • Omit the minor or patch portion of the semver tag to receive updates for the omitted portion (eg: 13 will represent the latest minor + patch release of v13).
  • +
+
+
+

Updating only specific containers

+

By default the watchtower service will check every 24 hours for new image updates to pull, based on currently running containers (not restricted to only those running within your compose.yaml).

+

Images eligible for updates can configured with a custom command that provides a list of container names, or via other supported options (eg: labels). This configuration is detailed in the watchtower docs.

+
+
+

Manual cleanup

+

watchtower also supports running on-demand with docker run or compose.yaml via the --run-once option.

+

You can alternatively invoke cleanup of Docker storage directly with:

+ +

If you omit the --all option, this will instead only remove "dangling" content (eg: Orphaned images).

+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/advanced/optional-config/index.html b/v13.1/config/advanced/optional-config/index.html new file mode 100644 index 00000000..ebb31b43 --- /dev/null +++ b/v13.1/config/advanced/optional-config/index.html @@ -0,0 +1,2046 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 docker-data/dms/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 if SSL_TYPE is set to self-signed or custom. (Docs: SSL)
  • +
  • rspamd: Override directory for custom settings when using Rspamd (Docs: Rspamd)
  • +
+

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)
  • +
  • rspamd/custom-commands.conf: list of simple commands to adjust Rspamd modules in an easy way (Docs: Rspamd)
  • +
+ + + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/advanced/override-defaults/dovecot/index.html b/v13.1/config/advanced/override-defaults/dovecot/index.html new file mode 100644 index 00000000..030d1f98 --- /dev/null +++ b/v13.1/config/advanced/override-defaults/dovecot/index.html @@ -0,0 +1,2058 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Override the Default Configs | Dovecot - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Dovecot

+ +

Add Configuration

+

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

+

Your DMS folder structure should look like this example:

+
├── docker-data/dms/config
+│   ├── dovecot.cf
+│   ├── postfix-accounts.cf
+│   └── postfix-virtual.cf
+├── compose.yaml
+└── 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 DMS 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.

+
services:
+  mailserver:
+    volumes:
+      - ./docker-data/dms/mail-data/:/var/mail/
+      - ./docker-data/dms/config/dovecot/10-master.conf:/etc/dovecot/conf.d/10-master.conf
+
+

You will first need to obtain the configuration from the running container (where mailserver is the container name):

+
mkdir -p ./docker-data/dms/config/dovecot
+docker cp mailserver:/etc/dovecot/conf.d/10-master.conf ./docker-data/dms/config/dovecot/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 DMS repository. Make sure to use the one matching your image version release.

+
+

The file docker-data/dms/config/dovecot.cf is copied internally to /etc/dovecot/local.conf. To verify the file content, run:

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

Postfix

+ +

Our default Postfix configuration can easily be extended to add parameters or modify existing ones by providing a docker-data/dms/config/postfix-main.cf. This file uses the same format as Postfix main.cf does (See official docs for all parameters and syntax rules).

+
+

Example

+

One can easily increase the backwards-compatibility level and set new Postscreen options:

+
# increase the compatibility level from 2 (default) to 3
+compatibility_level = 3
+# set a threshold value for Spam detection
+postscreen_dnsbl_threshold = 4
+
+
+
+

How are your changes applied?

+

The custom configuration you supply is appended to the default configuration located at /etc/postfix/main.cf, and then postconf -nf is run to remove earlier duplicate entries that have since been replaced. This happens early during container startup before Postfix is started.

+
+
+

Similarly, it is possible to add a custom docker-data/dms/config/postfix-master.cf file that will override the standard master.cf. Note: Each line in this file will be passed to postconf -P, i.e. the file is not appended as a whole to /etc/postfix/master.cf like docker-data/dms/config/postfix-main.cf! The expected format is <service_name>/<type>/<parameter>, for example:

+
# adjust the submission "reject_unlisted_recipient" option
+submission/inet/smtpd_reject_unlisted_recipient=no
+
+
+

Attention

+

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

+
+

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

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

Modifications via Script

+ +

If you'd like to change, patch or alter files or behavior of DMS, 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 docker-data/dms/config/user-patches.sh in order to create the user-patches.sh script.

+

If you are managing your directory structure yourself, create a docker-data/dms/config/ directory and add the user-patches.sh file yourself.

+
# 1. Either create the docker-data/dms/config/ directory yourself
+#    or let docker-mailserver create it on initial startup
+/tmp $ mkdir -p docker-data/dms/config/ && cd docker-data/dms/config/
+
+# 2. Create the user-patches.sh file and edit it
+/tmp/docker-data/dms/config $ touch user-patches.sh
+/tmp/docker-data/dms/config $ nano 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/v13.1/config/advanced/podman/index.html b/v13.1/config/advanced/podman/index.html new file mode 100644 index 00000000..1a99f8d1 --- /dev/null +++ b/v13.1/config/advanced/podman/index.html @@ -0,0 +1,2252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Advanced | Podman - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Podman

+ +

Introduction

+

Podman is a daemonless container engine for developing, managing, and running OCI Containers on your Linux System.

+
+

About Support for Podman

+

Please note that Podman is not officially supported as DMS is built and verified on top of the Docker Engine. This content is entirely community supported. If you find errors, please open an issue and provide a PR.

+
+
+

About this Guide

+

This guide was tested with Fedora 34 using systemd and firewalld. Moreover, it requires Podman version >= 3.2. You may be able to substitute dnf - Fedora's package manager - with others such as apt.

+
+
+

About Security

+

Running podman in rootless mode requires additional modifications in order to keep your mailserver secure. +Make sure to read the related documentation.

+
+

Installation in Rootfull Mode

+

While using Podman, you can just manage docker-mailserver as what you did with Docker. Your best friend setup.sh includes the minimum code in order to support Podman since it's 100% compatible with the Docker CLI.

+

The installation is basically the same. Podman v3.2 introduced a RESTful API that is 100% compatible with the Docker API, so you can use Docker Compose with Podman easily. Install Podman and Docker Compose with your package manager first.

+
sudo dnf install podman docker-compose
+
+

Then enable podman.socket using systemctl.

+
systemctl enable --now podman.socket
+
+

This will create a unix socket locate under /run/podman/podman.sock, which is the entrypoint of Podman's API. Now, configure docker-mailserver and start it.

+
export DOCKER_HOST="unix:///run/podman/podman.sock"
+docker compose up -d mailserver
+docker compose ps
+
+

You should see that docker-mailserver is running now.

+

Self-start in Rootfull Mode

+

Podman is daemonless, that means if you want docker-mailserver self-start while boot up the system, you have to generate a systemd file with Podman CLI.

+
podman generate systemd mailserver > /etc/systemd/system/mailserver.service
+systemctl daemon-reload
+systemctl enable --now mailserver.service
+
+

Installation in Rootless Mode

+

Running rootless containers is one of Podman's major features. But due to some restrictions, deploying docker-mailserver in rootless mode is not as easy compared to rootfull mode.

+
    +
  • a rootless container is running in a user namespace so you cannot bind ports lower than 1024
  • +
  • a rootless container's systemd file can only be placed in folder under ~/.config
  • +
  • a rootless container can result in an open relay, make sure to read the security section.
  • +
+

Also notice that Podman's rootless mode is not about running as a non-root user inside the container, but about the mapping of (normal, non-root) host users to root inside the container.

+
+

Warning

+

In order to make rootless DMS work we must modify some settings in the Linux system, it requires some basic linux server knowledge so don't follow this guide if you not sure what this guide is talking about. Podman rootfull mode and Docker are still good and security enough for normal daily usage.

+
+

First, enable podman.socket in systemd's userspace with a non-root user.

+
systemctl enable --now --user podman.socket
+
+

The socket file should be located at /var/run/user/$(id -u)/podman/podman.sock. Then, modify compose.yaml to make sure all ports are bindings are on non-privileged ports.

+
services:
+  mailserver:
+    ports:
+      - "10025:25"   # SMTP  (explicit TLS => STARTTLS)
+      - "10143:143"  # IMAP4 (explicit TLS => STARTTLS)
+      - "10465:465"  # ESMTP (implicit TLS)
+      - "10587:587"  # ESMTP (explicit TLS => STARTTLS)
+      - "10993:993"  # IMAP4 (implicit TLS)
+
+

Then, setup your mailserver.env file follow the documentation and use Docker Compose to start the container.

+
export DOCKER_HOST="unix:///var/run/user/$(id -u)/podman/podman.sock"
+docker compose up -d mailserver
+docker compose ps
+
+

Security in Rootless Mode

+

In rootless mode, podman resolves all incoming IPs as localhost, which results in an open gateway in the default configuration. There are two workarounds to fix this problem, both of which have their own drawbacks.

+

Enforce authentication from localhost

+

The PERMIT_DOCKER variable in the mailserver.env file allows to specify trusted networks that do not need to authenticate. If the variable is left empty, only requests from localhost and the container IP are allowed, but in the case of rootless podman any IP will be resolved as localhost. Setting PERMIT_DOCKER=none enforces authentication also from localhost, which prevents sending unauthenticated emails.

+

Use the slip4netns network driver

+

The second workaround is slightly more complicated because the compose.yaml has to be modified. +As shown in the fail2ban section the slirp4netns network driver has to be enabled. +This network driver enables podman to correctly resolve IP addresses but it is not compatible with +user defined networks which might be a problem depending on your setup.

+

Rootless Podman requires adding the value slirp4netns:port_handler=slirp4netns to the --network CLI option, or network_mode setting in your compose.yaml.

+

You must also add the ENV NETWORK_INTERFACE=tap0, because Podman uses a hard-coded interface name for slirp4netns.

+
+

Example

+
services:
+  mailserver:
+    network_mode: "slirp4netns:port_handler=slirp4netns"
+    environment:
+      - NETWORK_INTERFACE=tap0
+      ...
+
+
+
+

Note

+

podman-compose is not compatible with this configuration.

+
+

Self-start in Rootless Mode

+

Generate a systemd file with the Podman CLI.

+
podman generate systemd mailserver > ~/.config/systemd/user/mailserver.service
+systemctl --user daemon-reload
+systemctl enable --user --now mailserver.service
+
+

Systemd's user space service is only started when a specific user logs in and stops when you log out. In order to make it to start with the system, we need to enable linger with loginctl

+
loginctl enable-linger <username>
+
+

Remember to run this command as root user.

+

Port Forwarding

+

When it comes to forwarding ports using firewalld, see https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/securing_networks/using-and-configuring-firewalld_securing-networks#port-forwarding_using-and-configuring-firewalld for more information.

+
firewall-cmd --permanent --add-forward-port=port=<25|143|465|587|993>:proto=<tcp>:toport=<10025|10143|10465|10587|10993>
+...
+
+# After you set all ports up.
+firewall-cmd --reload
+
+

Notice that this will only open the access to the external client. If you want to access privileges port in your server, do this:

+
firewall-cmd --permanent --direct --add-rule <ipv4|ipv6> nat OUTPUT 0 -p <tcp|udp> -o lo --dport <25|143|465|587|993> -j REDIRECT --to-ports <10025|10143|10465|10587|10993>
+...
+# After you set all ports up.
+firewall-cmd --reload
+
+

Just map all the privilege port with non-privilege port you set in compose.yaml before as root user.

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

Auto-Discovery of Services

+

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 URI. 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/v13.1/config/best-practices/dkim_dmarc_spf/index.html b/v13.1/config/best-practices/dkim_dmarc_spf/index.html new file mode 100644 index 00000000..4874e60c --- /dev/null +++ b/v13.1/config/best-practices/dkim_dmarc_spf/index.html @@ -0,0 +1,2397 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DKIM, DMARC & SPF - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

DKIM, DMARC & SPF

+

Cloudflare has written an article about DKIM, DMARC and SPF that we highly recommend you to read to get acquainted with the topic.

+
+

Rspamd vs Individual validators

+

With v12.0.0, Rspamd was integrated into DMS. It can perform validations for DKIM, DMARC and SPF as part of the spam-score-calculation for an email. DMS provides individual alternatives for each validation that can be used instead of deferring to Rspamd:

+
    +
  • DKIM: opendkim is used as a milter (like Rspamd)
  • +
  • DMARC: opendmarc is used as a milter (like Rspamd)
  • +
  • SPF: policyd-spf is used in Postfix's smtpd_recipient_restrictions
  • +
+

In a future release Rspamd will become the default for these validations, with a deprecation notice issued prior to the removal of the above alternatives.

+

We encourage everyone to prefer Rspamd via ENABLE_RSPAMD=1.

+
+
+

DNS Caches & Propagation

+

While modern DNS providers are quick, it may take minutes or even hours for new DNS records to become available / propagate.

+
+

DKIM

+
+

What is DKIM

+

DomainKeys Identified Mail (DKIM) is an email authentication method designed to detect forged sender addresses in email (email spoofing), a technique often used in phishing and email spam.

+

Source

+
+

When DKIM is enabled:

+
    +
  1. Inbound mail will verify any included DKIM signatures
  2. +
  3. Outbound mail is signed (when you're sending domain has a configured DKIM key)
  4. +
+

DKIM requires a public/private key pair to enable signing (via private key) your outgoing mail, while the receiving end must query DNS to verify (via public key) that the signature is trustworthy.

+

Generating Keys

+

You'll need to repeat this process if you add any new domains.

+

You should have:

+ +
+

Creating DKIM Keys

+

DKIM keys can be generated with good defaults by running:

+
docker exec -it <CONTAINER NAME> setup config dkim
+
+

If you need to generate your keys with different settings, check the help output for supported config options and examples:

+
docker exec -it <CONTAINER NAME> setup config dkim help
+
+

As described by the help output, you may need to use the domain option explicitly when you're using LDAP or Rspamd.

+
+
+Changing the key size +

The keypair generated for using with DKIM presently defaults to RSA-2048. This is a good size but you can lower the security to 1024-bit, or increase it to 4096-bit (discouraged as that is excessive).

+

To generate a key with different size (for RSA 1024-bit) run:

+
setup config dkim keysize 1024
+
+
+

RSA Key Sizes >= 4096 Bit

+

According to RFC 8301, keys are preferably between 1024 and 2048 bits. Keys of size 4096-bit or larger may not be compatible to all systems your mail is intended for.

+

You should not need a key length beyond 2048-bit. If 2048-bit does not meet your security needs, you may want to instead consider adopting key rotation or switching from RSA to ECC keys for DKIM.

+
+
+
+You may need to specify mail domains explicitly +

Required when using LDAP and Rspamd.

+

setup config dkim will generate DKIM keys for what is assumed as the primary mail domain (derived from the FQDN assigned to DMS, minus any subdomain).

+

When the DMS FQDN is mail.example.com or example.com, by default this command will generate DKIM keys for example.com as the primary domain for your users mail accounts (eg: hello@example.com).

+

The DKIM generation does not have support to query LDAP for additionanl mail domains it should know about. If the primary mail domain is not sufficient, then you must explicitly specify any extra domains via the domain option:

+
# ENABLE_OPENDKIM=1 (default):
+setup config dkim domain 'example.com,another-example.com'
+
+# ENABLE_RSPAMD=1 + ENABLE_OPENDKIM=0:
+setup config dkim domain example.com
+setup config dkim domain another-example.com
+
+
+

OpenDKIM with ACCOUNT_PROVISIONER=FILE

+

When DMS uses this configuration, it will by default also detect mail domains (from accounts added via setup email add), generating additional DKIM keys.

+
+
+

DKIM is currently supported by either OpenDKIM or Rspamd:

+
+
+
+

OpenDKIM is currently enabled by default.

+

After running setup config dkim, your new DKIM key files (and OpenDKIM config) have been added to /tmp/docker-mailserver/opendkim/.

+
+

Restart required

+

After restarting DMS, outgoing mail will now be signed with your new DKIM key(s) 🎉

+
+
+
+

Requires opt-in via ENABLE_RSPAMD=1 (and disable the default OpenDKIM: ENABLE_OPENDKIM=0).

+

Rspamd provides DKIM support through two separate modules:

+
    +
  1. Verifying DKIM signatures from inbound mail is enabled by default.
  2. +
  3. Signing outbound mail with your DKIM key needs additional setup (key + dns + config).
  4. +
+
+Using Multiple Domains +

If you have multiple domains, you need to:

+
    +
  • Create a key wth docker exec -it <CONTAINER NAME> setup config dkim domain <DOMAIN> for each domain DMS should sign outgoing mail for.
  • +
  • Provide a custom dkim_signing.conf (for which an example is shown below), as the default config only supports one domain.
  • +
+
+
+

About the Helper Script

+

The script will persist the keys in /tmp/docker-mailserver/rspamd/dkim/. Hence, if you are already using the default volume mounts, the keys are persisted in a volume. The script also restarts Rspamd directly, so changes take effect without restarting DMS.

+

The script provides you with log messages along the way of creating keys. In case you want to read the complete log, use -v (verbose) or -vv (very verbose).

+
+

In case you have not already provided a default DKIM signing configuration, the script will create one and write it to /etc/rspamd/override.d/dkim_signing.conf. If this file already exists, it will not be overwritten.

+

When you're already using the rspamd/override.d/ directory, the file is created inside your volume and therefore persisted correctly. If you are not using rspamd/override.d/, you will need to persist the file yourself (otherwise it is lost on container restart).

+

An example of what a default configuration file for DKIM signing looks like can be found by expanding the example below.

+
+
+DKIM Signing Module Configuration Examples +

A simple configuration could look like this:

+
# documentation: https://rspamd.com/doc/modules/dkim_signing.html
+
+enabled = true;
+
+sign_authenticated = true;
+sign_local = true;
+
+use_domain = "header";
+use_redis = false; # don't change unless Redis also provides the DKIM keys
+use_esld = true;
+check_pubkey = true; # you want to use this in the beginning
+
+domain {
+    example.com {
+        path = "/tmp/docker-mailserver/rspamd/dkim/mail.private";
+        selector = "mail";
+    }
+}
+
+

As shown next:

+
    +
  • You can add more domains into the domain { ... } section (in the following example: example.com and example.org).
  • +
  • A domain can also be configured with multiple selectors and keys within a selectors [ ... ] array (in the following example, this is done for example.org).
  • +
+
# ...
+
+domain {
+    example.com {
+        path = /tmp/docker-mailserver/rspamd/example.com/ed25519.private";
+        selector = "dkim-ed25519";
+    }
+    example.org {
+        selectors [
+            {
+                path = "/tmp/docker-mailserver/rspamd/dkim/example.org/rsa.private";
+                selector = "dkim-rsa";
+            },
+            {
+                path = "/tmp/docker-mailserver/rspamd/dkim/example.org/ed25519.private";
+                selector = "dkim-ed25519";
+            }
+        ]
+    }
+}
+
+
+
+Support for DKIM Keys using ED25519 +

This modern elliptic curve is supported by Rspamd, but support by third-parties for verifying Ed25519 DKIM signatures is unreliable.

+

If you sign your mail with this key type, you should include RSA as a fallback, like shown in the above example.

+
+
+Let Rspamd Check Your Keys +

When check_pubkey = true; is set, Rspamd will query the DNS record for each DKIM selector, verifying each public key matches the private key configured.

+

If there is a mismatch, a warning will be emitted to the Rspamd log /var/log/mail/rspamd.log.

+
+
+
+
+

DNS Record

+

When mail signed with your DKIM key is sent from your mail server, the receiver needs to check a DNS TXT record to verify the DKIM signature is trustworthy.

+
+

Configuring DNS - DKIM record

+

When you generated your key in the previous step, the DNS data was saved into a file <selector>.txt (default: mail.txt). Use this content to update your DNS via Web Interface or directly edit your DNS Zone file:

+
+
+
+

Create a new record:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldValue
TypeTXT
Name<selector>._domainkey (default: mail._domainkey)
TTLUse the default (otherwise 3600 seconds is appropriate)
DataFile content within ( ... ) (formatted as advised below)
+

When using Rspamd, the helper script has already provided you with the contents (the "Data" field) of the DNS record you need to create - you can just copy-paste this text.

+
+
+

<selector>.txt is already formatted as a snippet for adding to your DNS Zone file.

+

Just copy/paste the file contents into your existing DNS zone. The TXT value has been split into separate strings every 255 characters for compatibility.

+
+
+
+
+
+<selector>.txt - Formatting the TXT record value correctly +

This file was generated for use within a DNS zone file. The file name uses the DKIM selector it was generated with (default DKIM selector is mail, which creates mail.txt_).

+

For your DNS setup, DKIM support needs to create a TXT record to store the public key for mail clients to use. TXT records with values that are longer than 255 characters need to be split into multiple parts. This is why the generated <selector>.txt file (containing your public key for use with DKIM) has multiple value parts wrapped within double-quotes between ( and ).

+

A DNS web-interface may handle this separation internally instead, and could expect the value provided all as a single line instead of split. When that is required, you'll need to manually format the value as described below.

+

Your generated DNS record file (<selector>.txt) should look similar to this:

+
mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
+"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ"
+"5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB"
+) ;
+
+

Take the content between ( ... ), and combine all the quote wrapped content and remove the double-quotes including the white-space between them. That is your TXT record value, the above example would become this:

+
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB
+
+

To test that your new DKIM record is correct, query it with the dig command. The TXT value response should be a single line split into multiple parts wrapped in double-quotes:

+
$ dig +short TXT mail._domainkey.example.com
+"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39" "KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB"
+
+
+

Troubleshooting

+

MxToolbox has a DKIM Verifier that you can use to check your DKIM DNS record(s).

+

When using Rspamd, we recommend you turn on check_pubkey = true; in dkim_signing.conf. Rspamd will then check whether your private key matches your public key, and you can check possible mismatches by looking at /var/log/mail/rspamd.log.

+

DMARC

+

With DMS, DMARC is pre-configured out of the box. You may disable extra and excessive DMARC checks when using Rspamd via ENABLE_OPENDMARC=0.

+

The only thing you need to do in order to enable DMARC on a "DNS-level" is to add new TXT. In contrast to DKIM, DMARC DNS entries do 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 this one).

+

Typically something like this should be good to start with:

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

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

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

The DMARC status may not be displayed instantly due to delays in DNS (caches). Dmarcian has a few tools you can use to verify your DNS records.

+

SPF

+
+

What is SPF

+

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.

+

Source

+
+
+

Disabling policyd-spf?

+

As of now, policyd-spf cannot be disabled. This is WIP.

+
+

Adding an SPF Record

+

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

+
example.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 policyd-spf

+

For whitelisting an 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 at docker-data/dms/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.not-example.com,mx2.not-example.com
+
+

Then add this line to compose.yaml:

+
volumes:
+  - ./docker-data/dms/config/postfix-policyd-spf.conf:/etc/postfix-policyd-spf-python/policyd-spf.conf
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/debugging/index.html b/v13.1/config/debugging/index.html new file mode 100644 index 00000000..2e6e31ab --- /dev/null +++ b/v13.1/config/debugging/index.html @@ -0,0 +1,2268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Debugging - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Debugging

+ +

This page contains valuable information when it comes to resolving issues you encounter.

+
+

Contributions Welcome!

+

Please consider contributing solutions to the FAQ ❤

+
+

Preliminary Checks

+
    +
  • Check that all published DMS ports are actually open and not blocked by your ISP / hosting provider.
  • +
  • SSL errors are likely the result of a wrong setup on the user side and not caused by DMS itself.
  • +
  • Ensure that you have correctly started DMS. Many problems related to configuration are due to this.
  • +
+
+

Correctly starting DMS

+

Use the --force-recreate option to avoid configuration mishaps: docker compose up --force-recreate

+

Alternatively, always use docker compose down to stop DMS. Do not rely on CTRL + C, docker compose stop, or docker compose restart.

+
+

DMS setup scripts are run when a container starts, but may fail to work properly if you do the following:

+
    +
  • Stopping a container with commands like: docker stop or docker compose up stopped via CTRL + C instead of docker compose down.
  • +
  • Restarting a container.
  • +
+

Volumes persist data across container instances, however the same container instance will keep internal changes not stored in a volume until the container is removed.

+

Due to this, DMS setup scripts may modify configuration it has already modified in the past.

+
    +
  • This is brittle as some changes are naive by assuming they are applied to the original configs from the image.
  • +
  • Volumes in compose.yaml are expected to persist any important data. Thus it should be safe to throwaway the container created each time, avoiding this config problem.
  • +
+
+

Mail sent from DMS does not arrive at destination

+

Some service providers block outbound traffic on port 25. Common hosting providers known to have this issue:

+ +

These links may advise how the provider can unblock the port through additional services offered, or via a support ticket request.

+

Mail sent to DMS does not get delivered to user

+

Common logs related to this are:

+
    +
  • warning: do not list domain domain.fr in BOTH mydestination and virtual_mailbox_domains
  • +
  • Recipient address rejected: User unknown in local recipient table
  • +
+

If your logs look like this, you likely have assigned the same FQDN to the DMS hostname and your mail accounts which is not supported by default. You can either adjust your DMS hostname or follow this FAQ advice

+

It is also possible that DMS services are temporarily unavailable when configuration changes are detected, producing the 2nd error. Certificate updates may be a less obvious trigger.

+

Steps for Debugging DMS

+
    +
  1. Increase log verbosity: Very helpful for troubleshooting problems during container startup. Set the environment variable LOG_LEVEL to debug or trace.
  2. +
  3. Use error logs as a search query: Try finding an existing issue or search engine result from any errors in your container log output. Often you'll find answers or more insights. If you still need to open an issue, sharing links from your search may help us assist you. The mail server log can be acquired by running docker log <CONTAINER NAME> (or docker logs -f <CONTAINER NAME> if you want to follow the log).
  4. +
  5. Inspect the logs of the service that is failing: We provide a dedicated paragraph on this topic further down below.
  6. +
  7. Understand the basics of mail servers: Especially for beginners, make sure you read our Introduction and Usage articles.
  8. +
  9. Search the whole FAQ: Our FAQ contains answers for common problems. Make sure you go through the list.
  10. +
  11. Reduce the scope: Ensure that you can run a basic setup of DMS first. Then incrementally restore parts of your original configuration until the problem is reproduced again. If you're new to DMS, it is common to find the cause is misunderstanding how to configure a minimal setup.
  12. +
+

Debug a running container

+

General

+

To get a shell inside the container run: docker exec -it <CONTAINER NAME> bash. To install additional software, run:

+
    +
  1. apt-get update to update repository metadata.
  2. +
  3. apt-get install <PACKAGE> to install a package, e.g., apt-get install neovim if you want to use NeoVim instead of nano (which is shipped by default).
  4. +
+

Logs

+

If you need more flexibility than what the docker logs command offers, then the most useful locations to get relevant DMS logs within the container are:

+
    +
  • /var/log/mail/<SERVICE>.log
  • +
  • /var/log/supervisor/<SERVICE>.log
  • +
+

You may use nano (a text editor) to edit files, while less (a file viewer) and tail/cat are useful tools to inspect the contents of logs.

+

Compatibility

+

It's possible that the issue you're experiencing is due to a compatibility conflict.

+

This could be from outdated software, or running a system that isn't able to provide you newer software and kernels. You may want to verify if you can reproduce the issue on a system that is not affected by these concerns.

+

Network

+
    +
  • Misconfigured network connections can cause the client IP address to be proxied through a docker network gateway IP, or a service that acts on behalf of connecting clients for logins where the connections client IP appears to be only from that service (eg: Container IP) instead. This can relay the wrong information to other services (eg: monitoring like Fail2Ban, SPF verification) causing unexpected failures.
  • +
  • userland-proxy: Prior to Docker v23, changing the userland-proxy setting did not reliably remove NAT rules.
  • +
  • UFW / firewalld: Some users expect only their firewall frontend to manage the firewall rules, but these will be bypassed when Docker publishes a container port (as there is no integration between the two).
  • +
  • iptables / nftables: +
  • +
  • IPv6:
      +
    • Requires additional configuration to prevent or properly support IPv6 connections (eg: Preserving the Client IP).
    • +
    • Support in 2023 is still considered experimental. You are advised to use at least Docker Engine v23 (2023Q1).
    • +
    • Various networking bug fixes have been addressed since the initial IPv6 support arrived in Docker Engine v20.10.0 (2020Q4).
    • +
    +
  • +
+

System

+
    +
  • macOS: DMS has limited support for macOS. Often an issue encountered is due to permissions related to the volumes config in compose.yaml. You may have luck trying gRPC FUSE as the file sharing implementation; VirtioFS is the successor but presently appears incompatible with DMS.
  • +
  • Kernel: Some systems provide kernels with modifications (replacing defaults and backporting patches) to support running legacy software or kernels, complicating compatibility. This can be commonly experienced with products like NAS.
  • +
  • CGroups v2: Hosts running older kernels (prior to 5.2) and systemd (prior to v244) are not likely to leverage cgroup v2, or have not defaulted to the cgroup v2 unified hierarchy. Not meeting this baseline may influence the behaviour of your DMS container, even with the latest Docker Engine installed.
  • +
  • Container runtime: Docker and Podman for example have subtle differences. DMS docs are primarily focused on Docker, but we try to document known issues where relevant.
  • +
  • Rootless containers: Introduces additional differences in behaviour or requirements:
      +
    • cgroup v2 is required for supporting rootless containers.
    • +
    • Differences such as for container networking which may further affect support for IPv6 and preserving the client IP (Remote address). Example with Docker rootless are binding a port to a specific interface and the choice of port forwarding driver.
    • +
    +
  • +
+ + + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/environment/index.html b/v13.1/config/environment/index.html new file mode 100644 index 00000000..790b4b76 --- /dev/null +++ b/v13.1/config/environment/index.html @@ -0,0 +1,4845 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 ghcr.io/docker-mailserver/docker-mailserver:edge.

+
+

General

+
OVERRIDE_HOSTNAME
+

If you can't set your hostname (eg: you're in a container platform that doesn't let you) specify it via this environment variable. It will have priority over docker run --hostname, or the equivalent hostname: field in compose.yaml.

+
    +
  • empty => Uses the hostname -f command to get canonical hostname for DMS to use.
  • +
  • => Specify an FQDN (fully-qualified domain name) to serve mail for. The hostname is required for DMS to function correctly.
  • +
+
LOG_LEVEL
+

Set the log level for DMS. This is mostly relevant for container startup scripts and change detection event feedback.

+

Valid values (in order of increasing verbosity) are: error, warn, info, debug and trace. The default log level is info.

+
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.

+
DMS_VMAIL_UID
+

Default: 5000

+

The User ID assigned to the static vmail user for /var/mail (Mail storage managed by Dovecot).

+
DMS_VMAIL_GID
+

Default: 5000

+

The Group ID assigned to the static vmail group for /var/mail (Mail storage managed by Dovecot).

+
ONE_DIR
+
    +
  • 0 => state in default directories.
  • +
  • 1 => consolidate all states into a single directory (/var/mail-state) to allow persistence using docker volumes. See the related FAQ entry for more information.
  • +
+
ACCOUNT_PROVISIONER
+

Configures the provisioning source of user accounts (including aliases) for user queries and authentication by services managed by DMS (Postfix and Dovecot).

+

User provisioning via OIDC is planned for the future, see this tracking issue.

+
    +
  • empty => use FILE
  • +
  • LDAP => use LDAP authentication
  • +
  • OIDC => use OIDC authentication (not yet implemented)
  • +
  • FILE => use local files (this is used as the default)
  • +
+

A second container for the ldap service is necessary (e.g. bitnami/openldap).

+
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.

+
    +
  • none => Explicitly force authentication
  • +
  • container => Container IP address 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.

+
TZ
+

Set the timezone. If this variable is unset, the container runtime will try to detect the time using /etc/localtime, which you can alternatively mount into the container. The value of this variable must follow the pattern AREA/ZONE, i.e. of you want to use Germany's time zone, use Europe/Berlin. You can lookup all available timezones here.

+
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_DNSBL
+

This enables DNS block lists in Postscreen. If you want to know which lists we are using, have a look at the default main.cf for Postfix we provide and search for postscreen_dnsbl_sites.

+
+

A Warning On DNS Block Lists

+

Make sure your DNS queries are properly resolved, i.e. you will most likely not want to use a public DNS resolver as these queries do not return meaningful results. We try our best to only evaluate proper return codes - this is not a guarantee that all codes are handled fine though.

+

Note that emails will be rejected if they don't pass the block list checks!

+
+
    +
  • 0 => DNS block lists are disabled
  • +
  • 1 => DNS block lists are enabled
  • +
+
ENABLE_OPENDKIM
+

Enables the OpenDKIM service.

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

Enables the OpenDMARC service.

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

Enabled policyd-spf in Postfix's configuration. You will likely want to set this to 0 in case you're using Rspamd (ENABLE_RSPAMD=1).

+
    +
  • 0 => Disabled
  • +
  • 1 => Enabled
  • +
+
ENABLE_POP3
+
    +
  • 0 => POP3 service disabled
  • +
  • 1 => Enables POP3 service
  • +
+
ENABLE_IMAP
+
    +
  • 0 => Disabled
  • +
  • 1 => Enabled
  • +
+
ENABLE_CLAMAV
+
    +
  • 0 => ClamAV is disabled
  • +
  • 1 => ClamAV is enabled
  • +
+
ENABLE_FAIL2BAN
+
    +
  • 0 => fail2ban service disabled
  • +
  • 1 => Enables fail2ban service
  • +
+

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

+
cap_add:
+  - NET_ADMIN
+
+

Otherwise, nftables 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
+

In the majority of cases, you want letsencrypt or manual.

+

self-signed can be used for testing SSL until you provide a valid certificate, note that third-parties cannot trust self-signed certificates, do not use this type in production. custom is a temporary workaround that is not officially supported.

+
    +
  • empty => SSL disabled.
  • +
  • letsencrypt => Support for using certificates with Let's Encrypt provisioners. (Docs: Let's Encrypt Setup)
  • +
  • manual => Provide your own certificate via separate key and cert files. (Docs: Bring Your Own Certificates)
      +
    • 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.
    • +
    +
  • +
  • custom => Provide your own certificate as a single file containing both the private key and full certificate chain. (Docs: None)
  • +
  • self-signed => Provide your own self-signed certificate files. Expects a self-signed CA cert for verification. Use only for local testing of your setup. (Docs: 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.

+
    +
  • 0 => (not recommended) Mail address spoofing allowed. Any logged in user may create email messages with a forged sender address.
  • +
  • 1 => 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 DMS 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
  • +
+
POSTFIX_DAGENT
+

Configure Postfix virtual_transport to deliver mail to a different LMTP client (default is a unix socket to dovecot).

+

Provide any valid URI. Examples:

+
    +
  • empty => lmtp:unix:/var/run/dovecot/lmtp (default, configured in Postfix main.cf)
  • +
  • lmtp:unix:private/dovecot-lmtp (use socket)
  • +
  • lmtps:inet:<host>:<port> (secure lmtp with starttls)
  • +
  • lmtp:<kopano-host>:2003 (use kopano as mailstore)
  • +
+
POSTFIX_MAILBOX_SIZE_LIMIT
+

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

+
    +
  • 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!). Size is in bytes.

+
    +
  • empty => 10240000 (~10 MB)
  • +
+
CLAMAV_MESSAGE_SIZE_LIMIT
+

Mails larger than this limit won't be scanned. +ClamAV must be enabled (ENABLE_CLAMAV=1) for this.

+
    +
  • empty => 25M (25 MB)
  • +
+
ENABLE_MANAGESIEVE
+
    +
  • empty => Managesieve service disabled
  • +
  • 1 => Enables Managesieve on port 4190
  • +
+
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_REJECT_UNKNOWN_CLIENT_HOSTNAME
+

If enabled, employs reject_unknown_client_hostname to sender restrictions in Postfix's configuration.

+
    +
  • 0 => Disabled
  • +
  • 1 => Enabled
  • +
+
POSTFIX_INET_PROTOCOLS
+
    +
  • all => Listen on all interfaces.
  • +
  • ipv4 => Listen only on IPv4 interfaces. Most likely you want this behind Docker.
  • +
  • ipv6 => Listen only on IPv6 interfaces.
  • +
+

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

+
DOVECOT_INET_PROTOCOLS
+
    +
  • all => Listen on all interfaces
  • +
  • ipv4 => Listen only on IPv4 interfaces. Most likely you want this behind Docker.
  • +
  • ipv6 => Listen only on IPv6 interfaces.
  • +
+

Note: More information at https://dovecot.org/doc/dovecot-example.conf

+
MOVE_SPAM_TO_JUNK
+

When enabled, e-mails marked with the

+
    +
  1. X-Spam: Yes header added by Rspamd
  2. +
  3. X-Spam-Flag: YES header added by SpamAssassin (requires SPAMASSASSIN_SPAM_TO_INBOX=1)
  4. +
+

will be automatically moved to the Junk folder (with the help of a Sieve script).

+
    +
  • 0 => Spam messages will be delivered in the mailbox.
  • +
  • 1 => Spam messages will be delivered in the Junk folder.
  • +
+
MARK_SPAM_AS_READ
+

Enable to treat received spam as "read" (avoids notification to MUA client of new mail).

+

Mail is received as spam when it has been marked with either header:

+
    +
  1. X-Spam: Yes (by Rspamd)
  2. +
  3. +

    X-Spam-Flag: YES (by SpamAssassin - requires SPAMASSASSIN_SPAM_TO_INBOX=1)

    +
  4. +
  5. +

    0 => disabled

    +
  6. +
  7. 1 => Spam messages will be marked as read
  8. +
+

Rspamd

+
ENABLE_RSPAMD
+

Enable or disable Rspamd.

+
    +
  • 0 => disabled
  • +
  • 1 => enabled
  • +
+
ENABLE_RSPAMD_REDIS
+

Explicit control over running a Redis instance within the container. By default, this value will match what is set for ENABLE_RSPAMD.

+

The purpose of this setting is to opt-out of starting an internal Redis instance when enabling Rspamd, replacing it with your own external instance.

+
+Configuring Rspamd for an external Redis instance +

You will need to provide configuration at /etc/rspamd/local.d/redis.conf similar to:

+
servers = "redis.example.test:6379";
+expand_keys = true;
+
+
+
    +
  • 0 => Disabled
  • +
  • 1 => Enabled
  • +
+
RSPAMD_CHECK_AUTHENTICATED
+

This settings controls whether checks should be performed on emails coming from authenticated users (i.e. most likely outgoing emails). The default value is 0 in order to align better with SpamAssassin. We recommend reading through the Rspamd documentation on scanning outbound emails though to decide for yourself whether you need and want this feature.

+
+

Not all checks and actions are disabled

+

DKIM signing of e-mails will still happen.

+
+
    +
  • 0 => No checks will be performed for authenticated users
  • +
  • 1 => All default checks will be performed for authenticated users
  • +
+
RSPAMD_GREYLISTING
+

Controls whether the Rspamd Greylisting module is enabled. This module can further assist in avoiding spam emails by greylisting e-mails with a certain spam score.

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

When enabled,

+
    +
  1. the "autolearning" feature is turned on;
  2. +
  3. the Bayes classifier will be trained (with the help of Sieve scripts) when moving mails
      +
    1. from anywhere to the Junk folder (learning this email as spam);
    2. +
    3. from the Junk folder into the INBOX (learning this email as ham).
    4. +
    +
  4. +
+
+

Attention

+

As of now, the spam learning database is global (i.e. available to all users). If one user deliberately trains it with malicious data, then it will ruin your detection rate.

+

This feature is suitably only for users who can tell ham from spam and users that can be trusted.

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

Can be used to enable or disable the Hfilter group module. This is used by DMS to adjust the HFILTER_HOSTNAME_UNKNOWN symbol, increasing its default weight to act similar to Postfix's reject_unknown_client_hostname, without the need to outright reject a message.

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

Can be used to control the score when the HFILTER_HOSTNAME_UNKNOWN symbol applies. A higher score is more punishing. Setting it to 15 (the default score for rejecting an e-mail) is equivalent to rejecting the email when the check fails.

+

Default: 6 (which corresponds to the add_header action)

+

Reports

+
PFLOGSUMM_TRIGGER
+

Enables regular Postfix log summary ("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 Postfix log summary reports.

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

Sender address (FROM) for pflogsumm reports (if Postfix log summary reports are enabled).

+
    +
  • not set => Use REPORT_SENDER
  • +
  • => 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)
  • +
+
LOGWATCH_SENDER
+

Sender address (FROM) for logwatch reports if logwatch reports are enabled.

+
    +
  • not set => Use REPORT_SENDER
  • +
  • => Specify the sender address
  • +
+
REPORT_RECIPIENT
+

Defines who receives reports (if they are enabled).

+
    +
  • empty => Use POSTMASTER_ADDRESS
  • +
  • => Specify the recipient address
  • +
+
REPORT_SENDER
+

Defines who sends reports (if they are enabled).

+
    +
  • empty => mailserver-report@<YOUR DOMAIN>
  • +
  • => Specify the sender address
  • +
+
LOGROTATE_INTERVAL
+

Changes the interval in which log files are rotated.

+
    +
  • weekly => Rotate log files weekly
  • +
  • daily => Rotate log files daily
  • +
  • monthly => Rotate log files monthly
  • +
+
+

Note

+

LOGROTATE_INTERVAL only manages logrotate within the container for services we manage internally.

+

The entire log output for the container is still available via docker logs mailserver (or your respective container name). If you want to configure external log rotation for that container output as well, : Docker Logging Drivers.

+

By default, the logs are lost when the container is destroyed (eg: re-creating via docker compose down && docker compose up -d). To keep the logs, mount a volume (to /var/log/mail/).

+
+
+

Note

+

This variable can also determine the interval for Postfix's log summary reports, see PFLOGSUMM_TRIGGER.

+
+

SpamAssassin

+
ENABLE_SPAMASSASSIN
+
    +
  • 0 => SpamAssassin is disabled
  • +
  • 1 => SpamAssassin is enabled
  • +
+
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.
  • +
+
ENABLE_SPAMASSASSIN_KAM
+

KAM is a 3rd party SpamAssassin ruleset, provided by the McGrail Foundation. If SpamAssassin is enabled, KAM can be used in addition to the default ruleset.

+
    +
  • 0 => KAM disabled
  • +
  • 1 => KAM enabled
  • +
+
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
+
    +
  • 10.0 => triggers spam evasive actions
  • +
+
+

This SpamAssassin setting needs ENABLE_SPAMASSASSIN=1

+

By default, DMS 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. To inhibit this behaviour and deliver spam emails, set this to a very high value e.g. 100.0.

+

If ONE_DIR=1 (default) the location is /var/mail-state/lib-amavis/virusmails/, or if ONE_DIR=0: /var/lib/amavis/virusmails/. These paths are inside the docker container.

+
+
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 separate fetchmail instance is started to allow having multiple imap idle connections per server (when poll entries reference the same IMAP server).
  • +
+

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.

+

Getmail

+
ENABLE_GETMAIL
+

Enable or disable getmail.

+
    +
  • 0 => Disabled
  • +
  • 1 => Enabled
  • +
+
GETMAIL_POLL
+
    +
  • 5 => getmail The number of minutes for the interval. Min: 1; Max: 30; Default: 5.
  • +
+

LDAP

+
LDAP_START_TLS
+
    +
  • empty => no
  • +
  • yes => LDAP over TLS enabled for Postfix
  • +
+
LDAP_SERVER_HOST
+
    +
  • empty => mail.example.com
  • +
  • => Specify the <dns-name> / <ip-address> where the LDAP server is reachable via a URI like: ldaps://mail.example.com.
  • +
  • Note: You must include the desired URI scheme (ldap://, ldaps://, ldapi://).
  • +
+
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 specified in DOVECOT_DN.
  • +
+
DOVECOT_URIS
+
    +
  • empty => same as LDAP_SERVER_HOST
  • +
  • => Specify a space separated list of LDAP URIs.
  • +
  • Note: You must include the desired URI scheme (ldap://, ldaps://, ldapi://).
  • +
+
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: You must include the desired URI scheme (ldap://, ldaps://, ldapi://).
  • +
+
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
  • +
+
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 (internal), 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/v13.1/config/pop3/index.html b/v13.1/config/pop3/index.html new file mode 100644 index 00000000..a1da20a7 --- /dev/null +++ b/v13.1/config/pop3/index.html @@ -0,0 +1,1953 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 compose.yaml:

+
mailserver:
+  ports:
+    - "25:25"    # SMTP  (explicit TLS => STARTTLS)
+    - "143:143"  # IMAP4 (explicit TLS => STARTTLS)
+    - "465:465"  # ESMTP (implicit TLS)
+    - "587:587"  # ESMTP (explicit TLS => STARTTLS)
+    - "993:993"  # IMAP4 (implicit TLS)
+    - "110:110"  # POP3
+    - "995:995"  # POP3 (with TLS)
+  environment:
+    - ENABLE_POP3=1
+
+ + + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/security/fail2ban/index.html b/v13.1/config/security/fail2ban/index.html new file mode 100644 index 00000000..e9088373 --- /dev/null +++ b/v13.1/config/security/fail2ban/index.html @@ -0,0 +1,2178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Security | Fail2Ban - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Fail2Ban

+ +
+

What is Fail2Ban (F2B)?

+

Fail2ban is an intrusion prevention software framework. Written in the Python programming language, it is designed to prevent against brute-force attacks. It is able to run on POSIX systems that have an interface to a packet-control system or firewall installed locally, such as [NFTables] or TCP Wrapper.

+

Source

+
+

Configuration

+
+

Warning

+

DMS must be launched with the NET_ADMIN capability in order to be able to install the NFTables rules that actually ban IP addresses. Thus, either include --cap-add=NET_ADMIN in the docker run command, or the equivalent in the compose.yaml:

+
cap_add:
+  - NET_ADMIN
+
+
+
+

Running Fail2Ban on Older Kernels

+

DMS configures F2B to use NFTables, not IPTables (legacy). We have observed that older systems, for example NAS systems, do not support the modern NFTables rules. You will need to configure F2B to use legacy IPTables again, for example with the fail2ban-jail.cf, see the section on configuration further down below.

+
+

DMS Defaults

+

DMS will automatically ban IP addresses of hosts that have generated 6 failed attempts over the course of the last week. The bans themselves last for one week. The Postfix jail is configured to use mode = extra in DMS.

+

Custom Files

+ +

This following configuration files inside the docker-data/dms/config/ volume will be copied inside the container during startup

+
    +
  1. fail2ban-jail.cf is copied to /etc/fail2ban/jail.d/user-jail.local
      +
    • with this file, you can adjust the configuration of individual jails and their defaults
    • +
    • there is an example provided in our repository on GitHub
    • +
    +
  2. +
  3. fail2ban-fail2ban.cf is copied to /etc/fail2ban/fail2ban.local +
  4. +
+

Viewing All Bans

+

When just running

+
setup fail2ban
+
+

the script will show all banned IP addresses.

+

To get a more detailed status view, run

+
setup fail2ban status
+
+

Managing Bans

+

You can manage F2B with the setup script. The usage looks like this:

+
docker exec <CONTAINER NAME> setup fail2ban [<ban|unban> <IP>]
+
+

Viewing the Log File

+
docker exec <CONTAINER NAME> setup fail2ban log
+
+

Running Inside A Rootless Container

+

RootlessKit is the fakeroot implementation for supporting rootless mode in Docker and Podman. By default, RootlessKit uses the builtin port forwarding driver, which does not propagate source IP addresses.

+

It is necessary for F2B to have access to the real source IP addresses in order to correctly identify clients. This is achieved by changing the port forwarding driver to slirp4netns, which is slower than the builtin driver but does preserve the real source IPs.

+
+
+
+

For rootless mode in Docker, create ~/.config/systemd/user/docker.service.d/override.conf with the following content:

+
+

Danger

+

This changes the port driver for all rootless containers managed by Docker. Per container configuration is not supported, if you need that consider Podman instead.

+
+
[Service]
+Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns"
+
+

And then restart the daemon:

+
$ systemctl --user daemon-reload
+$ systemctl --user restart docker
+
+
+
+

Rootless Podman requires adding the value slirp4netns:port_handler=slirp4netns to the --network CLI option, or network_mode setting in your compose.yaml:

+
+

Example

+
services:
+  mailserver:
+    network_mode: "slirp4netns:port_handler=slirp4netns"
+    environment:
+      - ENABLE_FAIL2BAN=1
+      - NETWORK_INTERFACE=tap0
+      ...
+
+
+

You must also add the ENV NETWORK_INTERFACE=tap0, because Podman uses a hard-coded interface name for slirp4netns. slirp4netns is not compatible with user-defined networks!

+
+
+
+ + + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/security/mail_crypt/index.html b/v13.1/config/security/mail_crypt/index.html new file mode 100644 index 00000000..5c0dc988 --- /dev/null +++ b/v13.1/config/security/mail_crypt/index.html @@ -0,0 +1,2047 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/

+
+

Single Encryption Key / Global Method

+
    +
  1. +

    Create 10-custom.conf and populate it with the following:

    +
    # Enables mail_crypt for all services (imap, pop3, etc)
    +mail_plugins = $mail_plugins mail_crypt
    +plugin {
    +  mail_crypt_global_private_key = </certs/ecprivkey.pem
    +  mail_crypt_global_public_key = </certs/ecpubkey.pem
    +  mail_crypt_save_version = 2
    +}
    +
    +
  2. +
  3. +

    Shutdown your mailserver (docker compose down)

    +
  4. +
  5. +

    You then need to generate your global EC key. We named them /certs/ecprivkey.pem and /certs/ecpubkey.pem in step #1.

    +
  6. +
  7. +

    The EC key needs to be available in the container. I prefer to mount a /certs directory into the container: +

    services:
    +  mailserver:
    +    image: ghcr.io/docker-mailserver/docker-mailserver:latest
    +    volumes:
    +    . . .
    +      - ./certs/:/certs
    +    . . .
    +

    +
  8. +
  9. +

    While you're editing the compose.yaml, add the configuration file: +

    services:
    +  mailserver:
    +    image: ghcr.io/docker-mailserver/docker-mailserver:latest
    +    volumes:
    +    . . .
    +      - ./config/dovecot/10-custom.conf:/etc/dovecot/conf.d/10-custom.conf
    +      - ./certs/:/certs
    +    . . .
    +

    +
  10. +
  11. +

    Start the container, monitor the logs for any errors, send yourself a message, and then confirm the file on disk is encrypted: +

    [root@ip-XXXXXXXXXX ~]# cat -A /mnt/efs-us-west-2/maildata/awesomesite.com/me/cur/1623989305.M6v�z�@�� m}��,��9����B*�247.us-west-2.compute.inE��\Ck*�@7795,W=7947:2,
    +T�9�8t�6�� t���e�W��S   `�H��C�ڤ �yeY��XZ��^�d�/��+�A
    +

    +
  12. +
+

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

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/security/rspamd/index.html b/v13.1/config/security/rspamd/index.html new file mode 100644 index 00000000..62b08ed6 --- /dev/null +++ b/v13.1/config/security/rspamd/index.html @@ -0,0 +1,2437 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Security | Rspamd - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Rspamd

+ +

About

+

Rspamd is a "fast, free and open-source spam filtering system". DMS integrates Rspamd like any other service. We provide a very simple but easy to maintain setup of Rspamd.

+

If you want to have a look at the default configuration files for Rspamd that DMS packs, navigate to target/rspamd/ inside the repository. Please consult the section "The Default Configuration" section down below for a written overview.

+ +

The following environment variables are related to Rspamd:

+
    +
  1. ENABLE_RSPAMD
  2. +
  3. ENABLE_RSPAMD_REDIS
  4. +
  5. RSPAMD_CHECK_AUTHENTICATED
  6. +
  7. RSPAMD_GREYLISTING
  8. +
  9. RSPAMD_HFILTER
  10. +
  11. RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE
  12. +
  13. RSPAMD_LEARN
  14. +
  15. MOVE_SPAM_TO_JUNK
  16. +
  17. MARK_SPAM_AS_READ
  18. +
+

With these variables, you can enable Rspamd itself and you can enable / disable certain features related to Rspamd.

+

The Default Configuration

+

Mode of Operation

+

The proxy worker operates in self-scan mode. This simplifies the setup as we do not require a normal worker. You can easily change this though by overriding the configuration by DMS.

+

DMS does not set a default password for the controller worker. You may want to do that yourself. In setups where you already have an authentication provider in front of the Rspamd webpage, you may want to set the secure_ip option to "0.0.0.0/0" for the controller worker to disable password authentication inside Rspamd completely.

+

Persistence with Redis

+

When Rspamd is enabled, we implicitly also start an instance of Redis in the container. Redis is configured to persist it's data via RDB snapshots to disk in the directory /var/lib/redis (which is a symbolic link to /var/mail-state/lib-redis/ when ONE_DIR=1 and a volume is mounted to /var/mail-state/). With the volume mount the snapshot will restore the Redis data across container restarts, and provide a way to keep backup.

+

Redis uses /etc/redis/redis.conf for configuration. We adjust this file when enabling the internal Redis service. If you have an external instance of Redis to use, the internal Redis service can be opt-out via setting the ENV ENABLE_RSPAMD_REDIS=0 (link also details required changes to the DMS rspamd config).

+

Web Interface

+

Rspamd provides a web interface, which contains statistics and data Rspamd collects. The interface is enabled by default and reachable on port 11334.

+

Rspamd Web Interface

+

DNS

+

DMS does not supply custom values for DNS servers to Rspamd. If you need to use custom DNS servers, which could be required when using DNS-based black/whitelists, you need to adjust options.inc yourself.

+
+

Making DNS Servers Configurable

+

If you want to see an environment variable (like RSPAMD_DNS_SERVERS) to support custom DNS servers for Rspamd being added to DMS, please raise a feature request issue.

+
+
+

Danger

+

While we do not provide values for custom DNS servers by default, we set soft_reject_on_timeout = true; by default. This setting will cause a soft reject if a task (presumably a DNS request) timeout takes place.

+

This setting is enabled to not allow spam to proceed just because DNS requests did not succeed. It could deny legitimate e-mails to pass though too in case your DNS setup is incorrect or not functioning properly.

+
+

Logs

+

You can find the Rspamd logs at /var/log/mail/rspamd.log, and the corresponding logs for Redis, if it is enabled, at /var/log/supervisor/rspamd-redis.log. We recommend inspecting these logs (with docker exec -it <CONTAINER NAME> less /var/log/mail/rspamd.log) in case Rspamd does not work as expected.

+

Modules

+

You can find a list of all Rspamd modules on their website.

+

Disabled By Default

+

DMS disables certain modules (clickhouse, elastic, neural, reputation, spamassassin, url_redirector, metric_exporter) by default. We believe these are not required in a standard setup, and they would otherwise needlessly use system resources.

+

Anti-Virus (ClamAV)

+

You can choose to enable ClamAV, and Rspamd will then use it to check for viruses. Just set the environment variable ENABLE_CLAMAV=1.

+

RBLs (Realtime Blacklists) / DNSBLs (DNS-based Blacklists)

+

The RBL module is enabled by default. As a consequence, Rspamd will perform DNS lookups to a variety of blacklists. Whether an RBL or a DNSBL is queried depends on where the domain name was obtained: RBL servers are queried with IP addresses extracted from message headers, DNSBL server are queried with domains and IP addresses extracted from the message body [source].

+
+

Rspamd and DNS Block Lists

+

When the RBL module is enabled, Rspamd will do a variety of DNS requests to (amongst other things) DNSBLs. There are a variety of issues involved when using DNSBLs. Rspamd will try to mitigate some of them by properly evaluating all return codes. This evaluation is a best effort though, so if the DNSBL operators change or add return codes, it may take a while for Rspamd to adjust as well.

+

If you want to use DNSBLs, try to use your own DNS resolver and make sure it is set up correctly, i.e. it should be a non-public & recursive resolver. Otherwise, you might not be able (see this Spamhaus post) to make use of the block lists.

+
+

Providing Custom Settings & Overriding Settings

+

DMS brings sane default settings for Rspamd. They are located at /etc/rspamd/local.d/ inside the container (or target/rspamd/local.d/ in the repository).

+

Manually

+ +

If you want to overwrite the default settings and / or provide your own settings, you can place files at docker-data/dms/config/rspamd/override.d/. Files from this directory are copied to /etc/rspamd/override.d/ during startup. These files forcibly override Rspamd and DMS default settings.

+
+

Clashing Overrides

+

Note that when also using the custom-commands.conf file, files in override.d may be overwritten in case you adjust them manually and with the help of the file.

+
+

With the Help of a Custom File

+

DMS provides the ability to do simple adjustments to Rspamd modules with the help of a single file. Just place a file called custom-commands.conf into docker-data/dms/config/rspamd/. If this file is present, DMS will evaluate it. The structure is very simple. Each line in the file looks like this:

+
COMMAND ARGUMENT1 ARGUMENT2 ARGUMENT3
+
+

where COMMAND can be:

+
    +
  1. disable-module: disables the module with name ARGUMENT1
  2. +
  3. enable-module: explicitly enables the module with name ARGUMENT1
  4. +
  5. set-option-for-module: sets the value for option ARGUMENT2 to ARGUMENT3 inside module ARGUMENT1
  6. +
  7. set-option-for-controller: set the value of option ARGUMENT1 to ARGUMENT2 for the controller worker
  8. +
  9. set-option-for-proxy: set the value of option ARGUMENT1 to ARGUMENT2 for the proxy worker
  10. +
  11. set-common-option: set the option ARGUMENT1 that defines basic Rspamd behaviour to value ARGUMENT2
  12. +
  13. add-line: this will add the complete line after ARGUMENT1 (with all characters) to the file /etc/rspamd/override.d/<ARGUMENT1>
  14. +
+
+

An Example Is Shown Down Below

+
+
+

File Names & Extensions

+

For command 1 - 3, we append the .conf suffix to the module name to get the correct file name automatically. For commands 4 - 6, the file name is fixed (you don't even need to provide it). For command 7, you will need to provide the whole file name (including the suffix) yourself!

+
+

You can also have comments (the line starts with #) and blank lines in custom-commands.conf - they are properly handled and not evaluated.

+
+

Adjusting Modules This Way

+

These simple commands are meant to give users the ability to easily alter modules and their options. As a consequence, they are not powerful enough to enable multi-line adjustments. If you need to do something more complex, we advise to do that manually!

+
+

Examples & Advanced Configuration

+

A Very Basic Configuration

+

You want to start using Rspamd? Rspamd is disabled by default, so you need to set the following environment variables:

+
ENABLE_RSPAMD=1
+ENABLE_OPENDKIM=0
+ENABLE_OPENDMARC=0
+ENABLE_POLICYD_SPF=0
+ENABLE_AMAVIS=0
+ENABLE_SPAMASSASSIN=0
+
+

This will enable Rspamd and disable services you don't need when using Rspamd.

+

Adjusting and Extending The Very Basic Configuration

+

Rspamd is running, but you want or need to adjust it? First, create a file named custom-commands.conf under docker-data/dms/config/rspamd (which translates to /tmp/docker-mailserver/rspamd/ inside the container). Then add you changes:

+
    +
  1. Say you want to be able to easily look at the frontend Rspamd provides on port 11334 (default) without the need to enter a password (maybe because you already provide authorization and authentication). You will need to adjust the controller worker: set-option-for-controller secure_ip "0.0.0.0/0".
  2. +
  3. You additionally want to enable the auto-spam-learning for the Bayes module? No problem: set-option-for-module classifier-bayes autolearn true.
  4. +
  5. But the chartable module gets on your nerves? Easy: disable-module chartable.
  6. +
+
+What Does the Result Look Like? +

Here is what the file looks like in the end:

+
# See 1.
+# ATTENTION: this disables authentication on the website - make sure you know what you're doing!
+set-option-for-controller secure_ip "0.0.0.0/0"
+
+# See 2.
+set-option-for-module classifier-bayes autolearn true
+
+# See 3.
+disable-module chartable
+
+
+

DKIM Signing

+

There is a dedicated section for setting up DKIM with Rspamd in our documentation.

+

Abusix Integration

+

This subsection gives information about the integration of Abusix, "a set of blocklists that work as an additional email security layer for your existing mail environment". The setup is straight-forward and well documented:

+
    +
  1. Create an account
  2. +
  3. Retrieve your API key
  4. +
  5. Navigate to the "Getting Started" documentation for Rspamd and follow the steps described there
  6. +
  7. Make sure to change <APIKEY> to your private API key
  8. +
+

We recommend mounting the files directly into the container, as they are rather big and not manageable with the modules script. If mounted to the correct location, Rspamd will automatically pick them up.

+

While Abusix can be integrated into Postfix, Postscreen and a multitude of other software, we recommend integrating Abusix only into a single piece of software running in your mail server - everything else would be excessive and wasting queries. Moreover, we recommend the integration into suitable filtering software and not Postfix itself, as software like Postscreen or Rspamd can properly evaluate the return codes and other configuration.

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

SSL/TLS

+ +

There are multiple options to enable SSL (via SSL_TYPE):

+ +

After installation, you can test your setup with:

+ +
+

Exposure of DNS labels through Certificate Transparency

+

All public Certificate Authorities (CAs) are required to log certificates they issue publicly via Certificate Transparency. This helps to better establish trust.

+

When using a public CA for certificates used in private networks, be aware that the associated DNS labels in the certificate are logged publicly and easily searchable. These logs are append only, you cannot redact this information.

+

You could use a wildcard certificate. This avoids accidentally leaking information to the internet, but keep in mind the potential security risks of wildcard certs.

+
+

The FQDN

+

An FQDN (Fully Qualified Domain Name) such as mail.example.com is required for DMS to function correctly, especially for looking up the correct SSL certificate to use.

+
    +
  • mail.example.com will still use user@example.com as the mail address. You do not need a bare domain for that.
  • +
  • We usually discourage assigning a bare domain (When your DNS MX record does not point to a subdomain) to represent DMS. However, an FQDN of just example.com is also supported.
  • +
  • Internally, hostname -f will be used to retrieve the FQDN as configured in the below examples.
  • +
  • Wildcard certificates (eg: *.example.com) are supported for SSL_TYPE=letsencrypt. Your configured FQDN below may be mail.example.com, and your wildcard certificate provisioned to /etc/letsencrypt/live/example.com which will be checked as a fallback FQDN by DMS.
  • +
+
+

Setting the hostname correctly

+

Change mail.example.com below to your own FQDN.

+
# CLI:
+docker run --hostname mail.example.com
+
+

or

+
# compose.yaml
+services:
+  mailserver:
+    hostname: mail.example.com
+
+
+

Provisioning methods

+ +

To enable Let's Encrypt for DMS, you have to:

+
    +
  1. Get your certificate using the Let's Encrypt client Certbot.
  2. +
  3. +

    For your DMS container:

    + +
  4. +
+

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

+
+

Note

+

/etc/letsencrypt/live stores provisioned certificates in individual folders named by their FQDN.

+

Make sure that the entire folder is mounted to DMS as there are typically symlinks from /etc/letsencrypt/live/mail.example.com to /etc/letsencrypt/archive.

+
+
+

Example

+

Add these additions to the mailserver service in your compose.yaml:

+
services:
+  mailserver:
+    hostname: mail.example.com
+    environment:
+      - SSL_TYPE=letsencrypt
+    volumes:
+      - /etc/letsencrypt:/etc/letsencrypt
+
+
+

Example using Docker for Let's Encrypt

+

Certbot provisions certificates to /etc/letsencrypt. Add a volume to store these, so that they can later be accessed by DMS container. You may also want to persist Certbot logs, just in case you need to troubleshoot.

+
    +
  1. +

    Getting a certificate is this simple! (Referencing: Certbot docker instructions and certonly --standalone mode):

    +
    # Requires access to port 80 from the internet, adjust your firewall if needed.
    +docker run --rm -it \
    +  -v "${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/" \
    +  -v "${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/" \
    +  -p 80:80 \
    +  certbot/certbot certonly --standalone -d mail.example.com
    +
    +
  2. +
  3. +

    Add a volume for DMS that maps the local certbot/certs/ folder to the container path /etc/letsencrypt/.

    +
    +

    Example

    +

    Add these additions to the mailserver service in your compose.yaml:

    +
    services:
    +  mailserver:
    +    hostname: mail.example.com
    +    environment:
    +      - SSL_TYPE=letsencrypt
    +    volumes:
    +      - ./docker-data/certbot/certs/:/etc/letsencrypt
    +
    +
    +
  4. +
  5. +

    The certificate setup is complete, but remember it will expire. Consider automating renewals.

    +
  6. +
+
+

Renewing Certificates

+

When running the above certonly --standalone snippet again, the existing certificate is renewed if it would expire within 30 days.

+

Alternatively, Certbot can look at all the certificates it manages, and only renew those nearing their expiry via the renew command:

+
# This will need access to port 443 from the internet, adjust your firewall if needed.
+docker run --rm -it \
+  -v "${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/" \
+  -v "${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/" \
+  -p 80:80 \
+  -p 443:443 \
+  certbot/certbot renew
+
+

This process can also be automated via cron or systemd timers.

+
+
+

Using a different ACME CA

+

Certbot does support alternative certificate providers via the --server option. In most cases you'll want to use the default Let's Encrypt.

+
+

Example using certbot-dns-cloudflare with Docker

+

If you are unable get a certificate via the HTTP-01 (port 80) or TLS-ALPN-01 (port 443) challenge types, the DNS-01 challenge can be useful (this challenge can additionally issue wildcard certificates). This guide shows how to use the DNS-01 challenge with Cloudflare as your DNS provider.

+

Obtain a Cloudflare API token:

+
    +
  1. Login into your Cloudflare dashboard.
  2. +
  3. Navigate to the API Tokens page.
  4. +
  5. +

    Click "Create Token", and choose the Edit zone DNS template (Certbot requires the ZONE:DNS:Edit permission).

    +
    +

    Only include the necessary Zone resource configuration

    +

    Be sure to configure "Zone Resources" section on this page to Include -> Specific zone -> <your zone here>.

    +

    This restricts the API token to only this zone (domain) which is an important security measure.

    +
    +
  6. +
  7. +

    Store the API token you received in a file cloudflare.ini with content:

    +
    dns_cloudflare_api_token = YOUR_CLOUDFLARE_TOKEN_HERE
    +
    +
      +
    • As this is sensitive data, you should restrict access to it with chmod 600 and chown 0:0.
    • +
    • Store the file in a folder if you like, such as docker-data/certbot/secrets/.
    • +
    +
  8. +
  9. +

    Your compose.yaml should include the following:

    +
    services:
    +  mailserver:
    +    environments:
    +      # Set SSL certificate type.
    +      - SSL_TYPE=letsencrypt
    +    volumes:
    +      # Mount the cert folder generated by Certbot:
    +      - ./docker-data/certbot/certs/:/etc/letsencrypt/:ro
    +
    +  certbot-cloudflare:
    +    image: certbot/dns-cloudflare:latest
    +    command: certonly --dns-cloudflare --dns-cloudflare-credentials /run/secrets/cloudflare-api-token -d mail.example.com
    +    volumes:
    +      - ./docker-data/certbot/certs/:/etc/letsencrypt/
    +      - ./docker-data/certbot/logs/:/var/log/letsencrypt/
    +    secrets:
    +      - cloudflare-api-token
    +
    +# Docs: https://docs.docker.com/engine/swarm/secrets/#use-secrets-in-compose
    +# WARNING: In compose configs without swarm, the long syntax options have no effect,
    +# Ensure that you properly `chmod 600` and `chown 0:0` the file on disk. Effectively treated as a bind mount.
    +secrets:
    +  cloudflare-api-token:
    +    file: ./docker-data/certbot/secrets/cloudflare.ini
    +
    +

    Alternative using the docker run command (secrets feature is not available):

    +
    docker run \
    +  --volume "${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/" \
    +  --volume "${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/" \
    +  --volume "${PWD}/docker-data/certbot/secrets/:/tmp/secrets/certbot/"
    +  certbot/dns-cloudflare \
    +  certonly --dns-cloudflare --dns-cloudflare-credentials /tmp/secrets/certbot/cloudflare.ini -d mail.example.com
    +
    +
  10. +
  11. +

    Run the service to provision a certificate:

    +
    docker compose run certbot-cloudflare
    +
    +
  12. +
  13. +

    You should see the following log output:

    +
    Saving debug log to /var/log/letsencrypt/letsencrypt. log | Requesting a certificate for mail.example.com
    +Waiting 10 seconds for DNS changes to propagate
    +Successfully received certificate.
    +Certificate is saved at: /etc/letsencrypt/live/mail.example.com/fullchain.pem
    +Key is saved at: /etc/letsencrypt/live/mail.example.com/privkey.pem
    +This certificate expires on YYYY-MM-DD.
    +These files will be updated when the certificate renews.
    +NEXT STEPS:
    +- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal instructions.
    +
    +
  14. +
+

After completing the steps above, your certificate should be ready to use.

+
+Renewing a certificate (Optional) +

We've only demonstrated how to provision a certificate, but it will expire in 90 days and need to be renewed before then.

+

In the following example, add a new service (certbot-cloudflare-renew) into compose.yaml that will handle certificate renewals:

+
services:
+  certbot-cloudflare-renew:
+    image: certbot/dns-cloudflare:latest
+    command: renew --dns-cloudflare --dns-cloudflare-credentials /run/secrets/cloudflare-api-token
+    volumes:
+      - ./docker-data/certbot/certs/:/etc/letsencrtypt/
+      - ./docker-data/certbot/logs/:/var/log/letsencrypt/
+    secrets:
+      - cloudflare-api-token
+
+

You can manually run this service to renew the cert within 90 days:

+
docker compose run certbot-cloudflare-renew
+
+

You should see the following output +(The following log was generated with --dry-run options)

+
Saving debug log to /var/log/letsencrypt/letsencrypt.log
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Processing /etc/letsencrypt/renewal/mail.example.com.conf
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Account registered.
+Simulating renewal of an existing certificate for mail.example.com
+Waiting 10 seconds for DNS changes to propagate
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Congratulations, all simulated renewals succeeded:
+  /etc/letsencrypt/live/mail.example.com/fullchain.pem (success)
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+

It is recommended to automate this renewal via a task scheduler like a systemd timer or in crontab +(crontab example: Checks every day if the certificate should be renewed)

+
0 0 * * * docker compose -f PATH_TO_YOUR_DOCKER_COMPOSE_YML up certbot-cloudflare-renew
+
+
+

Example using nginx-proxy and acme-companion with Docker

+

If you are running a web server already, port 80 will be in use which Certbot requires. You could use the Certbot --webroot feature, but it is more common to leverage a reverse proxy that manages the provisioning and renewal of certificates for your services automatically.

+

In the following example, we show how DMS can be run alongside the docker containers nginx-proxy and acme-companion (Referencing: acme-companion documentation):

+
    +
  1. +

    Start the reverse proxy (nginx-proxy):

    +
    docker run --detach \
    +  --name nginx-proxy \
    +  --restart always \
    +  --publish 80:80 \
    +  --publish 443:443 \
    +  --volume "${PWD}/docker-data/nginx-proxy/html/:/usr/share/nginx/html/" \
    +  --volume "${PWD}/docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/" \
    +  --volume "${PWD}/docker-data/acme-companion/certs/:/etc/nginx/certs/:ro" \
    +  --volume '/var/run/docker.sock:/tmp/docker.sock:ro' \
    +  nginxproxy/nginx-proxy
    +
    +
  2. +
  3. +

    Then start the certificate provisioner (acme-companion), which will provide certificates to nginx-proxy:

    +
    # Inherit `nginx-proxy` volumes via `--volumes-from`, but make `certs/` writeable:
    +docker run --detach \
    +  --name nginx-proxy-acme \
    +  --restart always \
    +  --volumes-from nginx-proxy \
    +  --volume "${PWD}/docker-data/acme-companion/certs/:/etc/nginx/certs/:rw" \
    +  --volume "${PWD}/docker-data/acme-companion/acme-state/:/etc/acme.sh/" \
    +  --volume '/var/run/docker.sock:/var/run/docker.sock:ro' \
    +  --env 'DEFAULT_EMAIL=admin@example.com' \
    +  nginxproxy/acme-companion
    +
    +
  4. +
  5. +

    Start the rest of your web server containers as usual.

    +
  6. +
  7. +

    Start a dummy container to provision certificates for your FQDN (eg: mail.example.com). acme-companion will detect the container and generate a Let's Encrypt certificate for your domain, which can be used by DMS:

    +
    docker run --detach \
    +  --name webmail \
    +  --env 'VIRTUAL_HOST=mail.example.com' \
    +  --env 'LETSENCRYPT_HOST=mail.example.com' \
    +  --env 'LETSENCRYPT_EMAIL=admin@example.com' \
    +  nginx
    +
    +

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

    +
  8. +
  9. +

    Make sure your mount path to the letsencrypt certificates directory is correct. Edit your compose.yaml for the mailserver service to have volumes added like below:

    +
    volumes:
    +  - ./docker-data/dms/mail-data/:/var/mail/
    +  - ./docker-data/dms/mail-state/:/var/mail-state/
    +  - ./docker-data/dms/config/:/tmp/docker-mailserver/
    +  - ./docker-data/acme-companion/certs/:/etc/letsencrypt/live/:ro
    +
    +
  10. +
  11. +

    Then from the compose.yaml project directory, run: docker compose up -d mailserver.

    +
  12. +
+

Example using nginx-proxy and acme-companion with docker-compose

+

The following example is the basic setup you need for using nginx-proxy and acme-companion with DMS (Referencing: acme-companion documentation):

+
+Example: compose.yaml +

You should have an existing compose.yaml with a mailserver service. Below are the modifications to add for integrating with nginx-proxy and acme-companion services:

+
services:
+  # Add the following `environment` and `volumes` to your existing `mailserver` service:
+  mailserver:
+    environment:
+      # SSL_TYPE:         Uses the `letsencrypt` method to find mounted certificates.
+      # VIRTUAL_HOST:     The FQDN that `nginx-proxy` will configure itself to handle for HTTP[S] connections.
+      # LETSENCRYPT_HOST: The FQDN for a certificate that `acme-companion` will provision and renew.
+      - SSL_TYPE=letsencrypt
+      - VIRTUAL_HOST=mail.example.com
+      - LETSENCRYPT_HOST=mail.example.com
+    volumes:
+      - ./docker-data/acme-companion/certs/:/etc/letsencrypt/live/:ro
+
+  # If you don't yet have your own `nginx-proxy` and `acme-companion` setup,
+  # here is an example you can use:
+  reverse-proxy:
+    image: nginxproxy/nginx-proxy
+    container_name: nginx-proxy
+    restart: always
+    ports:
+      # Port  80: Required for HTTP-01 challenges to `acme-companion`.
+      # Port 443: Only required for containers that need access over HTTPS. TLS-ALPN-01 challenge not supported.
+      - "80:80"
+      - "443:443"
+    volumes:
+      # `certs/`:      Managed by the `acme-companion` container (_read-only_).
+      # `docker.sock`: Required to interact with containers via the Docker API.
+      - ./docker-data/nginx-proxy/html/:/usr/share/nginx/html/
+      - ./docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/
+      - ./docker-data/acme-companion/certs/:/etc/nginx/certs/:ro
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+
+  acme-companion:
+    image: nginxproxy/acme-companion
+    container_name: nginx-proxy-acme
+    restart: always
+    environment:
+      # When `volumes_from: [nginx-proxy]` is not supported,
+      # reference the _reverse-proxy_ `container_name` here:
+      - NGINX_PROXY_CONTAINER=nginx-proxy
+    volumes:
+      # `html/`:       Write ACME HTTP-01 challenge files that `nginx-proxy` will serve.
+      # `vhost.d/`:    To enable web access via `nginx-proxy` to HTTP-01 challenge files.
+      # `certs/`:      To store certificates and private keys.
+      # `acme-state/`: To persist config and state for the ACME provisioner (`acme.sh`).
+      # `docker.sock`: Required to interact with containers via the Docker API.
+      - ./docker-data/nginx-proxy/html/:/usr/share/nginx/html/
+      - ./docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/
+      - ./docker-data/acme-companion/certs/:/etc/nginx/certs/:rw
+      - ./docker-data/acme-companion/acme-state/:/etc/acme.sh/
+      - /var/run/docker.sock:/var/run/docker.sock:ro
+
+
+
+

Optional ENV vars worth knowing about

+

Per container ENV that acme-companion will detect to override default provisioning settings:

+
    +
  • LETSENCRYPT_TEST=true: Recommended during initial setup. Otherwise the default production endpoint has a rate limit of 5 duplicate certificates per week. Overrides ACME_CA_URI to use the Let's Encrypt staging endpoint.
  • +
  • LETSENCRYPT_EMAIL: For when you don't use DEFAULT_EMAIL on acme-companion, or want to assign a different email contact for this container.
  • +
  • LETSENCRYPT_KEYSIZE: Allows you to configure the type (RSA or ECDSA) and size of the private key for your certificate. Default is RSA 4096.
  • +
  • LETSENCRYPT_RESTART_CONTAINER=true: When the certificate is renewed, the entire container will be restarted to ensure the new certificate is used.
  • +
+

acme-companion ENV for default settings that apply to all containers using LETSENCRYPT_HOST:

+
    +
  • DEFAULT_EMAIL: An email address that the CA (eg: Let's Encrypt) can contact you about expiring certificates, failed renewals, or for account recovery. You may want to use an email address not handled by your mail server to ensure deliverability in the event your mail server breaks.
  • +
  • CERTS_UPDATE_INTERVAL: If you need to adjust the frequency to check for renewals. 3600 seconds (1 hour) by default.
  • +
  • DEBUG=1: Should be helpful when troubleshooting provisioning issues from acme-companion logs.
  • +
  • ACME_CA_URI: Useful in combination with CA_BUNDLE to use a private CA. To change the default Let's Encrypt endpoint to the staging endpoint, use https://acme-staging-v02.api.letsencrypt.org/directory.
  • +
  • CA_BUNDLE: If you want to use a private CA instead of Let's Encrypt.
  • +
+
+
+

Alternative to required ENV on mailserver service

+

While you will still need both nginx-proxy and acme-companion containers, you can manage certificates without adding ENV vars to containers. Instead the ENV is moved into a file and uses the acme-companion feature Standalone certificates.

+

This requires adding another shared volume between nginx-proxy and acme-companion:

+
services:
+  reverse-proxy:
+    volumes:
+      - ./docker-data/nginx-proxy/conf.d/:/etc/nginx/conf.d/
+
+  acme-companion:
+    volumes:
+      - ./docker-data/nginx-proxy/conf.d/:/etc/nginx/conf.d/
+      - ./docker-data/acme-companion/standalone.sh:/app/letsencrypt_user_data:ro
+
+

acme-companion mounts a shell script (standalone.sh), which defines variables to customize certificate provisioning:

+
# A list IDs for certificates to provision:
+LETSENCRYPT_STANDALONE_CERTS=('mail')
+
+# Each ID inserts itself into the standard `acme-companion` supported container ENV vars below.
+# The LETSENCRYPT_<ID>_HOST var is a list of FQDNs to provision a certificate for as the SAN field:
+LETSENCRYPT_mail_HOST=('mail.example.com')
+
+# Optional variables:
+LETSENCRYPT_mail_TEST=true
+LETSENCRYPT_mail_EMAIL='admin@example.com'
+# RSA-4096 => `4096`, ECDSA-256 => `ec-256`:
+LETSENCRYPT_mail_KEYSIZE=4096
+
+

Unlike with the equivalent ENV for containers, changes to this file will not be detected automatically. You would need to wait until the next renewal check by acme-companion (every hour by default), restart acme-companion, or manually invoke the service loop:

+

docker exec nginx-proxy-acme /app/signal_le_service

+
+

Example using Let's Encrypt Certificates with 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 compose.yaml declaration file:

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

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

+

Caddy

+

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 example.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:

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

The generated certificates can then be mounted:

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

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 (Postfix and Dovecot).

+

Wildcard certificates are supported. If your FQDN is mail.example.com and your wildcard certificate is *.example.com, add the ENV: SSL_DOMAIN=example.com.

+

DMS will select it's certificate from acme.json checking these ENV for a matching FQDN (in order of priority):

+
    +
  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's Encrypt, i.e. Traefik will not issue a certificate without a service / router demanding it.

+
+Example Code +

Here is an example setup for docker-compose:

+
services:
+  mailserver:
+    image: ghcr.io/docker-mailserver/docker-mailserver:latest
+    container_name: mailserver
+    hostname: mail.example.com
+    volumes:
+      - ./docker-data/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
+
+  reverse-proxy:
+    image: docker.io/traefik:latest #v2.5
+    container_name: docker-traefik
+    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@example.com
+      - --certificatesresolvers.letsencrypt.acme.storage=/acme.json
+      - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http
+    volumes:
+      - ./docker-data/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.example.com`)"
+
+
+

Self-Signed Certificates

+
+

Warning

+

Use self-signed certificates only for testing purposes!

+
+

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

+
    +
  • <FQDN>-key.pem
  • +
  • <FQDN>-cert.pem
  • +
  • demoCA/cacert.pem
  • +
+

Where <FQDN> is the FQDN you've configured for your DMS container.

+

Add SSL_TYPE=self-signed to your DMS environment variables. Postfix and Dovecot will be configured to use the provided certificate (.pem files above) during container startup.

+

Generating a self-signed certificate

+

One way to generate self-signed certificates is with Smallstep's step CLI. This is exactly what DMS 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 (eg: docker-data/dms/custom-certs/), then use docker to run that script in a container:

+
# '--user' is to keep ownership of the files written to
+# the local volume to use your systems User and Group ID values.
+docker run --rm -it \
+  --user "$(id -u):$(id -g)" \
+  --volume "${PWD}/docker-data/dms/custom-certs/:/tmp/step-ca/" \
+  --workdir "/tmp/step-ca/" \
+  --entrypoint "/tmp/step-ca/generate-certs.sh" \
+  smallstep/step-ca
+
+

Bring Your Own Certificates

+

You can also provide your own certificate files. Add these entries to your compose.yaml:

+
volumes:
+  - ./docker-data/dms/custom-certs/:/tmp/dms/custom-certs/:ro
+environment:
+  - SSL_TYPE=manual
+  # Values should match the file paths inside the container:
+  - SSL_CERT_PATH=/tmp/dms/custom-certs/public.crt
+  - SSL_KEY_PATH=/tmp/dms/custom-certs/private.key
+
+

This will mount the path where your certificate files reside locally into the read-only container folder: /tmp/dms/custom-certs.

+

The local and internal paths may be whatever you prefer, so long as both SSL_CERT_PATH and SSL_KEY_PATH point to the correct internal file paths. The certificate files may also be named to your preference, but should be PEM encoded.

+

SSL_ALT_CERT_PATH and SSL_ALT_KEY_PATH are additional ENV vars to support a 2nd certificate as a fallback. Commonly known as hybrid or dual certificate support. This is useful for using a modern ECDSA as your primary certificate, and RSA as your fallback for older connections. They work in the same manner as the non-ALT versions.

+
+

Info

+

You may have to restart DMS once the certificates change.

+
+

Testing a Certificate is Valid

+
    +
  • +

    From your host:

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

    Or:

    +
    docker exec mailserver 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 mailserver 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 docker-data/dms/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.

+

This is a community contributed script, and in most cases you will have better support via our Change Detection service (automatic for SSL_TYPE of manual and letsencrypt) - Unless you're using LDAP which disables the service.

+
+

Script Compatibility

+
    +
  • Relies on private filepaths /etc/dms/tls/cert and /etc/dms/tls/key intended for internal use only.
  • +
  • Only supports hard-coded fullchain.key + privkey.pem as your mounted file names. That may not align with your provisioning method.
  • +
  • No support for ALT fallback certificates (for supporting dual/hybrid, RSA + ECDSA).
  • +
+
+

The steps to follow are these:

+
    +
  1. Transfer the new certificates to ./docker-data/dms/custom-certs/ (volume mounted to: /tmp/ssl/)
  2. +
  3. You should provide fullchain.key and privkey.pem
  4. +
  5. Place the script in ./docker-data/dms/config/ (volume mounted to: /tmp/docker-mailserver/)
  6. +
  7. Make the script executable (chmod +x tomav-renew-certs.sh)
  8. +
  9. Run the script: docker exec mailserver /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.example.com \
+  -connect 192.168.0.72:465 \
+  2>/dev/null | openssl x509
+
+# or
+
+openssl s_client \
+  -servername mail.example.com \
+  -connect mail.example.com:465 \
+  2>/dev/null | openssl x509
+
+

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

+
export SITE_URL="mail.example.com"
+export SITE_IP_URL="192.168.0.72" # can also use `mail.example.com`
+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 could be either:
+#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 internal container folder: /tmp/ssl.
  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:

+
# This script is run inside docker-mailserver via 'docker exec ...', using the 'mail' command to send alerts.
+## code below will alert if certificate expires in less than two weeks
+## please adjust variables!
+## make sure the 'mail -s' command works! Test!
+
+export SITE_URL="mail.example.com"
+export SITE_IP_URL="192.168.2.72" # can also use `mail.example.com`
+export SITE_SSL_PORT="993" # imap port dovecot
+# Below can be from a different domain; like your personal email, not handled by this docker-mailserver:
+export ALERT_EMAIL_ADDR="external-account@gmail.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 the 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
+
+

Custom DH Parameters

+

By default DMS uses ffdhe4096 from IETF RFC 7919. These are standardized pre-defined DH groups and the only available DH groups for TLS 1.3. It is discouraged to generate your own DH parameters as it is often less secure.

+

Despite this, if you must use non-standard DH parameters or you would like to swap ffdhe4096 for a different group (eg ffdhe2048); Add your own PEM encoded DH params file via a volume to /tmp/docker-mailserver/dhparams.pem. This will replace DH params for both Dovecot and Postfix services during container startup.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/security/understanding-the-ports/index.html b/v13.1/config/security/understanding-the-ports/index.html new file mode 100644 index 00000000..98075bdc --- /dev/null +++ b/v13.1/config/security/understanding-the-ports/index.html @@ -0,0 +1,2322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Security | Understanding the Ports - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Understanding the Ports

+ +

Quick Reference

+

Prefer ports with Implicit TLS ports, they're more secure than ports using Explicit TLS, and if you use a Reverse Proxy should be less hassle.

+

Overview of Email Ports

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProtocolExplicit TLS1Implicit TLSPurposeEnabled by Default
ESMTP25N/ATransfer2Yes
ESMTP5874653SubmissionYes
POP3110995RetrievalNo
IMAP4143993RetrievalYes
+
    +
  1. A connection may be secured over TLS when both ends support STARTTLS. On ports 110, 143 and 587, DMS will reject a connection that cannot be secured. Port 25 is required to support insecure connections.
  2. +
  3. Receives email, DMS 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).
  6. +
+
+Beware of outdated advice on port 465 +

There is a common misconception of this port due to it's history detailed by various communities and blogs articles on the topic (including by popular mail relay services).

+

Port 465 was briefly assigned the role of SMTPS in 1997 as an secure alternative to Port 25 between MTA exchanges. Then RFC 2487 (STARTTLS) while still in a draft status in late 1998 had IANA revoke the SMTPS assignment. The draft history was modified to exclude all mention of port 465 and SMTPS.

+

In 2018 RFC 8314 was published which revives Port 465 as an Implicit TLS alternative to Port 587 for mail submission. It details very clearly that gaining adoption of 465 as the preferred port will take time. IANA reassigned port 465 as the submissions service. Any unofficial usage as SMTPS is legacy and has been for over two decades.

+

Understand that port 587 is more broadly supported due to this history and that lots of software in that time has been built or configured with that port in mind. STARTTLS is known to have various CVEs discovered even in recent years, do not be misled by any advice implying it should be preferred over implicit TLS. Trust in more official sources, such as the config Postfix has which acknowledges the submissions port (465).

+
+

What Ports Should I Use? (SMTP)

+
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)

+

Mail arriving at your server will be processed and stored in a mailbox, or sent outbound to another mail server.

+
    +
  • Port 25:
      +
    • Think of this like a physical mailbox, anyone can deliver mail to you here. Typically most mail is delivered to you on this port.
    • +
    • DMS will actively filter email delivered on this port for spam or viruses, and refuse mail from known bad sources.
    • +
    • Connections to this port may be secure through STARTTLS, but is not mandatory as mail is allowed to arrive via an unencrypted connection.
    • +
    • It is possible for internal clients to submit mail to be sent outbound (without requiring authentication), but that is discouraged. Prefer the submission ports.
    • +
    +
  • +
  • Port 465 and 587:
      +
    • This is the equivalent of a post office box where you would send email to be delivered on your behalf (DMS is that metaphorical post office, aka the MTA).
    • +
    • These two ports are known as the submission ports, they enable mail to be sent outbound to another MTA (eg: Outlook or Gmail) but require authentication via a mail account.
    • +
    • For inbound traffic, this is relevant when you send mail from your MUA (eg: ThunderBird). It's also used when DMS is configured as a mail relay, or when you have a service sending transactional mail (eg: order confirmations, password resets, notifications) through DMS.
    • +
    • Prefer port 465 over port 587, as 465 provides Implicit TLS.
    • +
    +
  • +
+
+

Note

+

When submitting mail (inbound) to be sent (outbound), this involves two separate connections to negotiate and secure. There may be additional intermediary connections which DMS is not involved in, and thus unable to ensure encrypted transit throughout delivery.

+
+

Outbound Traffic (On the Right)

+

Mail being sent from your server is either being relayed through another MTA (eg: SendGrid), or direct to an MTA responsible for an email address (eg: Gmail).

+
    +
  • Port 25:
      +
    • As most MTA use port 25 to receive inbound mail, when no authenticated relay is involved this is the outbound port used.
    • +
    • Outbound traffic on this port is often blocked by service providers (eg: VPS, ISP) to prevent abuse by spammers. If the port cannot be unblocked, you will need to relay outbound mail through a service to send on your behalf.
    • +
    +
  • +
  • Port 465 and 587:
      +
    • Submission ports for outbound traffic establish trust to forward mail through a third-party relay service. This requires authenticating to an account on the relay service. The relay will then deliver the mail through port 25 on your behalf.
    • +
    • These are the two typical ports used, but smart hosts like SendGrid often document support for additional non-standard ports as alternatives if necessary.
    • +
    • Usually you'll only use these outbound ports for relaying. It is possible to deliver directly to the relevant MTA for email address, but requires having credentials for each MTA.
    • +
    +
  • +
+
+

Tip

+

DMS can function as a relay too, but professional relay services have a trusted reputation (which increases success of delivery).

+

An MTA with low reputation can affect if mail is treated as junk, or even rejected.

+
+
+

Note

+

At best, you can only ensure a secure connection between the MTA you directly connect to. The receiving MTA may relay that mail to another MTA (and so forth), each connection may not be enforcing TLS.

+
+

Explicit vs Implicit TLS

+

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

+

Communication on these ports begin in cleartext. Upgrading to an encrypted connection must be requested explicitly through the STARTTLS protocol and successfully negotiated.

+

Sometimes a reverse-proxy is involved, but is misconfigured or lacks support for the STARTTLS negotiation to succeed.

+
+

Note

+
    +
  • By default, DMS is configured to reject connections that fail to establish a secure connection (when authentication is required), rather than allow an insecure connection.
  • +
  • Port 25 does not require authentication. If STARTTLS is unsuccessful, mail can be received over an unencrypted connection. You can better secure this port between trusted parties with the addition of MTA-STS, STARTTLS Policy List, DNSSEC and DANE.
  • +
+
+
+

Warning

+

STARTTLS continues to have vulnerabilities found (Nov 2021 article), as per RFC 8314 (Section 4.1) you are encouraged to prefer Implicit TLS where possible.

+

Support for STARTTLS is not always implemented correctly, which can lead to leaking credentials (like a 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.

+
+

Implicit TLS - Enforced Encryption

+

Communication on these ports are always encrypted (enforced, thus implicit), avoiding the potential risks with STARTTLS (Explicit TLS).

+

While Explicit TLS can provide the same benefit (when STARTTLS is successfully negotiated), Implicit TLS more reliably avoids concerns with connection manipulation and compatibility.

+

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 for a Mail Server, 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 does not provide the equivalent safety that HTTPS does 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/v13.1/config/setup.sh/index.html b/v13.1/config/setup.sh/index.html new file mode 100644 index 00000000..bc868e2f --- /dev/null +++ b/v13.1/config/setup.sh/index.html @@ -0,0 +1,1955 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + About setup.sh - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

About setup.sh

+ +
+

Note

+

setup.sh is not required. We encourage you to use docker exec -ti <CONTAINER NAME> setup instead.

+
+
+

Warning

+

This script assumes Docker or Podman is used. You will not be able to use setup.sh with other container orchestration tools.

+
+

setup.sh is a script that is complimentary to the internal setup command in DMS.

+

It mostly provides the convenience of aliasing docker exec -ti <CONTAINER NAME> setup, inferring the container name of a running DMS instance or running a new instance and bind mounting necessary volumes implicitly.

+

It is intended to be run from the host machine, not from inside your running container. The latest version of the script is included in the DMS 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
+
+

For more information on using the script run: ./setup.sh help.

+ + + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/config/user-management/index.html b/v13.1/config/user-management/index.html new file mode 100644 index 00000000..a2f7ba53 --- /dev/null +++ b/v13.1/config/user-management/index.html @@ -0,0 +1,2197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + User Management - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

User Management

+

Accounts

+

Users (email accounts) are managed in /tmp/docker-mailserver/postfix-accounts.cf. The best way to manage accounts is to use the reliable setup command inside the container. Just run docker exec <CONTAINER NAME> setup help and have a look at the section about subcommands, specifically the email subcommand.

+

Adding a new Account

+

Via setup inside the container

+

You can add an account by running docker exec -ti <CONTAINER NAME> setup email add <NEW ADDRESS>. This method is strongly preferred.

+

Manually

+
+

Warning

+

This method is discouraged!

+
+

Alternatively, you may directly add the full email address and its encrypted password, separated by a pipe. To generate a new mail account data, directly from your host, you could for example run the following:

+
docker run --rm -it                                      \
+  --env MAIL_USER=user1@example.com                      \
+  --env MAIL_PASS=mypassword                             \
+  ghcr.io/docker-mailserver/docker-mailserver:latest     \
+  /bin/bash -c \
+    'echo "${MAIL_USER}|$(doveadm pw -s SHA512-CRYPT -u ${MAIL_USER} -p ${MAIL_PASS})" >>docker-data/dms/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 docker-data/dms/config/postfix-accounts.cf file of your running container.

+

The result could look like this:

+
user1@example.com|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1
+
+

Quotas

+
    +
  • 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 (PRs are welcome!).
  • +
+

Aliases

+

The best way to manage aliases is to use the reliable setup script inside the container. Just run docker exec <CONTAINER NAME> setup help and have a look at the section about subcommands, specifically the alias-subcommand.

+

About

+

You may read Postfix's documentation on virtual aliases first. 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 example.com as its domain:

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

Configuring RegExp Aliases

+

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

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

Address Tags (Extension Delimiters) as 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 Postfix's official documentation.

+
+

Note

+

If you do decide to change the configurable separator, you must add the same line to both docker-data/dms/config/postfix-main.cf and docker-data/dms/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/v13.1/contributing/general/index.html b/v13.1/contributing/general/index.html new file mode 100644 index 00000000..202696f1 --- /dev/null +++ b/v13.1/contributing/general/index.html @@ -0,0 +1,2017 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Contributing | General Information - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

General Information

+ +

Coding Style

+

When refactoring, writing or altering scripts or other files, 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 in scripts
  8. +
+

Documentation

+

Make sure to select edge in the dropdown menu at the top. Navigate to the page you would like to edit and click the edit button in the top right. This allows you to make changes and create a pull-request.

+

Alternatively you can make the changes locally. For that you'll need to have Docker installed. Navigate into the docs/ directory. Then run:

+
docker run --rm -it -p 8000:8000 -v "${PWD}:/docs" squidfunk/mkdocs-material
+
+

This serves the documentation on your local machine on port 8000. Each change will be hot-reloaded onto the page you view, just edit, save and look at the result.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/contributing/issues-and-pull-requests/index.html b/v13.1/contributing/issues-and-pull-requests/index.html new file mode 100644 index 00000000..e349abae --- /dev/null +++ b/v13.1/contributing/issues-and-pull-requests/index.html @@ -0,0 +1,2088 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 docs for your version (maybe latest), 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 DMS with the environment variable LOG_LEVEL set to debug or trace 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.

+

Filing a Bug Report

+

Thank you for participating in this project and reporting a bug. Docker Mail Server (DMS) is a community-driven project, and each contribution counts!

+

Maintainers and moderators are volunteers. We greatly appreciate reports that take the time to provide detailed information via the template, enabling us to help you in the best and quickest way. Ignoring the template provided may seem easier, but discourages receiving any support (via assignment of the label meta/no template - no support).

+

Markdown formatting can be used in almost all text fields (unless stated otherwise in the description).

+

Be as precise as possible, and if in doubt, it's best to add more information that too few.

+

When an option is marked with "not officially supported" / "unsupported", then support is dependent on availability from specific maintainers.

+

Pull Requests

+
+

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.

+
+

Submit a Pull-Request

+

The development workflow is the following:

+
    +
  1. Fork the project and clone your fork with git clone --recurse-submodules ... or run git submodule update --init --recursive after you cloned your fork
  2. +
  3. Write the code that is needed :D
  4. +
  5. Add integration tests if necessary
  6. +
  7. Prepare your environment and run linting and tests
  8. +
  9. Document your improvements if necessary (e.g. if you introduced new environment variables, describe those in the ENV documentation) and add your changes the changelog under the "Unreleased" section
  10. +
  11. 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.
  12. +
+

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.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/contributing/tests/index.html b/v13.1/contributing/tests/index.html new file mode 100644 index 00000000..8fe04ef8 --- /dev/null +++ b/v13.1/contributing/tests/index.html @@ -0,0 +1,2230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Tests

+ +
+

Program testing can be used to show the presence of bugs, but never to show their absence!

+

– Edsger Wybe Dijkstra

+
+

Introduction

+

DMS employs a variety of unit and integration tests. All tests and associated configuration is stored in the test/ directory. If you want to change existing functionality or integrate a new feature into DMS, you will probably need to work with our test suite.

+
+

Can I use macOS?

+

We do not support running linting, tests, etc. on macOS at this time. Please use a Linux VM, Debian/Ubuntu is recommended.

+
+

About

+

We use BATS (Bash Automated Testing System) and additional support libraries. BATS is very similar to Bash, and one can easily and quickly get an understanding of how tests in a single file are run. A test file template provides a minimal working example for newcomers to look at.

+

Structure

+

The test/ directory contains multiple directories. Among them is the bats/ directory (which is the BATS git submodule) and the helper/ directory. The latter is especially interesting because it contains common support functionality used in almost every test. Actual tests are located in test/tests/.

+
+

Test suite undergoing refactoring

+

We are currently in the process of restructuring all of our tests. Tests will be moved into test/tests/parallel/ and new tests should be placed there as well.

+
+

Using Our Helper Functions

+

There are many functions that aid in writing tests. We urge you to use them! They will not only ease writing a test but they will also do their best to ensure there are no race conditions or other unwanted side effects. To learn about the functions we provide, you can:

+
    +
  1. look into existing tests for helper functions we already used
  2. +
  3. look into the test/helper/ directory which contains all files that can (and will) be loaded in test files
  4. +
+

We encourage you to try both of the approaches mentioned above. To make understanding and using the helper functions easy, every function contains detailed documentation comments. Read them carefully!

+

How Are Tests Run?

+

Tests are split into two categories:

+
    +
  • test/tests/parallel/: Multiple test files are run concurrently to reduce the required time to complete the test suite. A test file will presently run it's own defined test-cases in a sequential order.
  • +
  • test/tests/serial/: Each test file is queued up to run sequentially. Tests that are unable to support running concurrently belong here.
  • +
+

Parallel tests are further partitioned into smaller sets. If your system has the resources to support running more than one of those sets at a time this is perfectly ok (our CI runs tests by distributing the sets across multiple test runners). Care must be taken not to mix running the serial tests while a parallel set is also running; this is handled for you when using make tests.

+

Running Tests

+

Prerequisites

+

To run the test suite, you will need to:

+
    +
  1. Install Docker
  2. +
  3. Install jq and (GNU) parallel (under Ubuntu, use sudo apt-get -y install jq parallel)
  4. +
  5. Execute git submodule update --init --recursive if you haven't already initialized the git submodules
  6. +
+

Executing Test(s)

+

We use make to run commands.

+
    +
  1. Run make build to create or update the local mailserver-testing:ci Docker image (using the projects Dockerfile)
  2. +
  3. Run all tests: make clean tests
  4. +
  5. Run a single test: make clean generate-accounts test/<TEST NAME WITHOUT .bats SUFFIX>
  6. +
  7. Run multiple unrelated tests: make clean generate-accounts test/<TEST NAME WITHOUT .bats SUFFIX>,<TEST NAME WITHOUT .bats SUFFIX> (just add a , and then immediately write the new test name)
  8. +
  9. Run a whole set or all serial tests: make clean generate-accounts tests/parallel/setX where X is the number of the set or make clean generate-accounts tests/serial
  10. +
+
+Setting the Degree of Parallelization for Tests +

If your machine is capable, you can increase the amount of tests that are run simultaneously by prepending the make clean all command with BATS_PARALLEL_JOBS=X (i.e. BATS_PARALLEL_JOBS=X make clean all). This wil speed up the test procedure. You can also run all tests in serial by setting BATS_PARALLEL_JOBS=1 this way.

+

The default value of BATS_PARALLEL_JOBS is 2. This can be increased if your system has the resources spare to support running more jobs at once to complete the test suite sooner.

+
+
+

Test Output when Running in Parallel

+

When running tests in parallel (with make clean generate-accounts tests/parallel/setX), BATS will delay outputting the results until completing all test cases within a file.

+

This likewise delays the reporting of test-case failures. When troubleshooting parallel set tests, you may prefer to run specific tests you're working on serially (as demonstrated in the example below).

+

When writing tests, ensure that parallel set tests still pass when run in parallel. You need to account for other tests running in parallel that may interfere with your own tests logic.

+
+
+

Tip

+

You may use make run-local-instance to run a version of the image built locally to test and edit your changes in a running DMS instance.

+
+

An Example

+

In this example, you've made a change to the Rspamd feature support (or adjusted it's tests). First verify no regressions have been introduced by running it's specific test file:

+
$ make clean generate-accounts test/rspamd
+rspamd.bats
+  ✓ [Rspamd] Postfix's main.cf was adjusted [12]
+  ✓ [Rspamd] normal mail passes fine [44]
+  ✓ [Rspamd] detects and rejects spam [122]
+  ✓ [Rspamd] detects and rejects virus [189]
+
+

As your feature work progresses your change for Rspamd also affects ClamAV. As your change now spans more than just the Rspamd test file, you could run multiple test files serially:

+
$ make clean generate-accounts test/rspamd,clamav
+rspamd.bats
+  ✓ [Rspamd] Postfix's main.cf was adjusted [12]
+  ✓ [Rspamd] normal mail passes fine [44]
+  ✓ [Rspamd] detects and rejects spam [122]
+  ✓ [Rspamd] detects and rejects virus [189]
+
+clamav.bats
+  ✓ [ClamAV] log files exist at /var/log/mail directory [68]
+  ✓ [ClamAV] should be identified by Amavis [67]
+  ✓ [ClamAV] freshclam cron is enabled [76]
+  ✓ [ClamAV] env CLAMAV_MESSAGE_SIZE_LIMIT is set correctly [63]
+  ✓ [ClamAV] rejects virus [60]
+
+

You're almost finished with your change before submitting it as a PR. It's a good idea to run the full parallel set those individual tests belong to (especially if you've modified any tests):

+
$ make clean generate-accounts tests/parallel/set1
+default_relay_host.bats
+  ✓ [Relay] (ENV) 'DEFAULT_RELAY_HOST' should configure 'main.cf:relayhost' [88]
+
+spam_virus/amavis.bats
+  ✓ [Amavis] SpamAssassin integration should be active [1165]
+
+spam_virus/clamav.bats
+  ✓ [ClamAV] log files exist at /var/log/mail directory [73]
+  ✓ [ClamAV] should be identified by Amavis [67]
+  ✓ [ClamAV] freshclam cron is enabled [76]
+...
+
+

Even better, before opening a PR run the full test suite:

+
$ make clean tests
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/examples/tutorials/basic-installation/index.html b/v13.1/examples/tutorials/basic-installation/index.html new file mode 100644 index 00000000..a0106462 --- /dev/null +++ b/v13.1/examples/tutorials/basic-installation/index.html @@ -0,0 +1,2214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tutorials | Basic Installation - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Basic Installation

+ +

A Basic Example With Relevant Environmental Variables

+

This example provides you only with a basic example of what a minimal setup could look like. We strongly recommend that you go through the configuration file yourself and adjust everything to your needs. The default compose.yaml can be used for the purpose out-of-the-box, see the Usage chapter.

+
services:
+  mailserver:
+    image: ghcr.io/docker-mailserver/docker-mailserver:latest
+    container_name: mailserver
+    # Provide the FQDN of your mail server here (Your DNS MX record should point to this value)
+    hostname: mail.example.com
+    ports:
+      - "25:25"
+      - "465:465"
+      - "587:587"
+      - "993:993"
+    volumes:
+      - ./docker-data/dms/mail-data/:/var/mail/
+      - ./docker-data/dms/mail-state/:/var/mail-state/
+      - ./docker-data/dms/mail-logs/:/var/log/mail/
+      - ./docker-data/dms/config/:/tmp/docker-mailserver/
+      - /etc/localtime:/etc/localtime:ro
+    environment:
+      - ENABLE_RSPAMD=1
+      - ENABLE_CLAMAV=1
+      - ENABLE_FAIL2BAN=1
+    cap_add:
+      - NET_ADMIN # For Fail2Ban to work
+    restart: always
+
+

A Basic LDAP Setup

+

Note There are currently no LDAP maintainers. If you encounter issues, please raise them in the issue tracker, but be aware that the core maintainers team will most likely not be able to help you. We would appreciate and we encourage everyone to actively participate in maintaining LDAP-related code by becoming a maintainer!

+
services:
+  mailserver:
+    image: ghcr.io/docker-mailserver/docker-mailserver:latest
+    container_name: mailserver
+    # Provide the FQDN of your mail server here (Your DNS MX record should point to this value)
+    hostname: mail.example.com
+    ports:
+      - "25:25"
+      - "465:465"
+      - "587:587"
+      - "993:993"
+    volumes:
+      - ./docker-data/dms/mail-data/:/var/mail/
+      - ./docker-data/dms/mail-state/:/var/mail-state/
+      - ./docker-data/dms/mail-logs/:/var/log/mail/
+      - ./docker-data/dms/config/:/tmp/docker-mailserver/
+      - /etc/localtime:/etc/localtime:ro
+    environment:
+      - ACCOUNT_PROVISIONER=LDAP
+      - LDAP_SERVER_HOST=ldap # your ldap container/IP/ServerName
+      - LDAP_SEARCH_BASE=ou=people,dc=localhost,dc=localdomain
+      - LDAP_BIND_DN=cn=admin,dc=localhost,dc=localdomain
+      - LDAP_BIND_PW=admin
+      - LDAP_QUERY_FILTER_USER=(&(mail=%s)(mailEnabled=TRUE))
+      - LDAP_QUERY_FILTER_GROUP=(&(mailGroupMember=%s)(mailEnabled=TRUE))
+      - LDAP_QUERY_FILTER_ALIAS=(|(&(mailAlias=%s)(objectClass=PostfixBookMailForward))(&(mailAlias=%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)))
+      - LDAP_QUERY_FILTER_DOMAIN=(|(&(mail=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailGroupMember=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailalias=*@%s)(objectClass=PostfixBookMailForward)))
+      - DOVECOT_PASS_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
+      - DOVECOT_USER_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
+      - ENABLE_SASLAUTHD=1
+      - SASLAUTHD_MECHANISMS=ldap
+      - SASLAUTHD_LDAP_SERVER=ldap
+      - SASLAUTHD_LDAP_BIND_DN=cn=admin,dc=localhost,dc=localdomain
+      - SASLAUTHD_LDAP_PASSWORD=admin
+      - SASLAUTHD_LDAP_SEARCH_BASE=ou=people,dc=localhost,dc=localdomain
+      - SASLAUTHD_LDAP_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%U))
+      - POSTMASTER_ADDRESS=postmaster@localhost.localdomain
+    restart: always
+
+

Using DMS as a local mail relay for containers

+
+

Info

+

This was originally a community contributed guide. Please let us know via a Github Issue if you're having any difficulty following the guide so that we can update it.

+
+

This guide is focused on only using SMTP ports (not POP3 and IMAP) with the intent to relay mail received from another service to an external email address (eg: user@gmail.com). It is not intended for mailbox storage of real users.

+

In this setup DMS is not intended to receive email from the outside world, so no anti-spam or anti-virus software is needed, making the service lighter to run.

+
+

setup

+

The setup command used below is to be run inside the container.

+
+
+

Open Relays

+

Adding the docker network's gateway to the list of trusted hosts (eg: 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.

+
+
    +
  1. +

    Create the file compose.yaml with a content like this:

    +
    +

    Example

    +
    services:
    +  mailserver:
    +    image: ghcr.io/docker-mailserver/docker-mailserver:latest
    +    container_name: mailserver
    +    # Provide the FQDN of your mail server here (Your DNS MX record should point to this value)
    +    hostname: mail.example.com
    +    ports:
    +      - "25:25"
    +      - "587:587"
    +      - "465:465"
    +    volumes:
    +      - ./docker-data/dms/mail-data/:/var/mail/
    +      - ./docker-data/dms/mail-state/:/var/mail-state/
    +      - ./docker-data/dms/mail-logs/:/var/log/mail/
    +      - ./docker-data/dms/config/:/tmp/docker-mailserver/
    +      - /etc/localtime:/etc/localtime:ro
    +    environment:
    +      - ENABLE_FAIL2BAN=1
    +      # Using letsencrypt for SSL/TLS certificates:
    +      - SSL_TYPE=letsencrypt
    +      # Allow sending emails from other docker containers:
    +      # Beware creating an Open Relay: https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#permit_docker
    +      - PERMIT_DOCKER=network
    +      # You may want to enable this: https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#spoof_protection
    +      # See step 6 below, which demonstrates setup with enabled/disabled SPOOF_PROTECTION:
    +      - SPOOF_PROTECTION=0
    +    cap_add:
    +      - NET_ADMIN # For Fail2Ban to work
    +    restart: always
    +
    +
    +

    The docs have a detailed page on Environment Variables for reference.

    +
    +Firewalled ports +

    If you have a firewall running, you may need to open ports 25, 587 and 465.

    +

    For example, with the firewall ufw, run:

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

    Caution: This may not be sound advice.

    +
    +
  2. +
  3. +

    Configure your DNS service to use an MX record for the hostname (eg: mail) you configured in the previous step and add the SPF TXT record.

    +
    +

    If you manually manage the DNS zone file for the domain

    +

    It would look something like this:

    +
    $ORIGIN example.com
    +@     IN  A      10.11.12.13
    +mail  IN  A      10.11.12.13
    +
    +; mail server for example.com
    +@     IN  MX  10 mail.example.com.
    +
    +; Add SPF record
    +@     IN  TXT    "v=spf1 mx -all"
    +
    +

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

    +
    +
  4. +
  5. +

    Generate DKIM keys for your domain via setup config dkim.

    +

    Copy the content of the file docker-data/dms/config/opendkim/keys/example.com/mail.txt and add it to your DNS records as a TXT like SPF was handled above.

    +

    I use bind9 for managing my domains, so I just paste it on example.com.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.com
    +
    +
  6. +
  7. +

    Get an SSL certificate, we have a guide for you here (Let's Encrypt is a popular service to get free SSL certificates).

    +
  8. +
  9. +

    Start DMS and check the terminal output for any errors: docker compose up.

    +
  10. +
  11. +

    Create email accounts and aliases:

    +
    +

    With SPOOF_PROTECTION=0

    +
    setup email add admin@example.com passwd123
    +setup email add info@example.com passwd123
    +setup alias add admin@example.com external-account@gmail.com
    +setup alias add info@example.com external-account@gmail.com
    +setup email list
    +setup alias list
    +
    +

    Aliases make sure that any email that comes to these accounts is forwarded to your third-party email address (external-account@gmail.com), where they are retrieved (eg: via third-party web or mobile app), instead of connecting directly to docker-mailserer with POP3 / IMAP.

    +
    +
    +

    With SPOOF_PROTECTION=1

    +
    setup email add admin.gmail@example.com passwd123
    +setup email add info.gmail@example.com passwd123
    +setup alias add admin@example.com admin.gmail@example.com
    +setup alias add info@example.com info.gmail@example.com
    +setup alias add admin.gmail@example.com external-account@gmail.com
    +setup alias add info.gmail@example.com external-account@gmail.com
    +setup email list
    +setup alias list
    +
    +

    This extra step is required to avoid the 553 5.7.1 Sender address rejected: not owned by user error (the accounts used for submitting mail to Gmail are admin.gmail@example.com and info.gmail@example.com)

    +
    +
  12. +
  13. +

    Send some test emails to these addresses and make other tests. Once everything is working well, stop the container with ctrl+c and start it again as a daemon: docker compose up -d.

    +
  14. +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/examples/tutorials/blog-posts/index.html b/v13.1/examples/tutorials/blog-posts/index.html new file mode 100644 index 00000000..b169ec06 --- /dev/null +++ b/v13.1/examples/tutorials/blog-posts/index.html @@ -0,0 +1,1942 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tutorials | Blog Posts - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/examples/tutorials/crowdsec/index.html b/v13.1/examples/tutorials/crowdsec/index.html new file mode 100644 index 00000000..0c6c25f6 --- /dev/null +++ b/v13.1/examples/tutorials/crowdsec/index.html @@ -0,0 +1,2099 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tutorials | Crowdsec - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Crowdsec

+ +
+

What is Crowdsec?

+

Crowdsec is an open source software that detects and blocks attackers using log analysis. +It has access to a global community-wide IP reputation database.

+

Source

+
+

Installation

+

Crowdsec supports multiple installation methods, however this page will use the docker installation.

+

Docker mailserver

+

In your compose.yaml for the DMS service, add a bind mount volume for /var/log/mail. This is to share the DMS logs to a separate crowdsec container.

+
+

Example

+
services:
+  mailserver:
+      - /docker-data/dms/mail-logs/:/var/log/mail/
+
+
+

Crowdsec

+

The crowdsec container should also bind mount the same host path for the DMS logs that was added in the DMS example above.

+
services:
+  image: crowdsecurity/crowdsec
+  restart: unless-stopped
+  ports:
+    - "8080:8080"
+    - "6060:6060"
+  volumes:
+    - /docker-data/dms/mail-logs/:/var/log/dms:ro
+    - ./acquis.d:/etc/crowdsec/acquis.d
+    - crowdsec-db:/var/lib/crowdsec/data/
+  environment:
+    # These collection contains parsers and scenarios for postfix and dovecot
+    COLLECTIONS: crowdsecurity/postfix crowdsecurity/dovecot
+    TZ: Europe/Paris
+volumes:
+  crowdsec-db:
+
+

Configuration

+

Configure crowdsec to read and parse DMS logs file.

+
+

Example

+

Create the file dms.yml in ./acquis.d/

+
---
+source: file
+filenames:
+  - /var/log/dms/mail.log
+labels:
+  type: syslog
+
+
+
+

Warning

+

Crowdsec on its own is just a detection software, the remediation is done by components called bouncers. +This page does not explain how to install or configure a bouncer. It can be found in crowdsec documentation.

+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/examples/tutorials/docker-build/index.html b/v13.1/examples/tutorials/docker-build/index.html new file mode 100644 index 00000000..23abf232 --- /dev/null +++ b/v13.1/examples/tutorials/docker-build/index.html @@ -0,0 +1,2101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tutorials | Docker Build - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Building your own Docker image

+ +

Building your own Docker image

+

Submodules

+

You'll need to retrieve the git submodules prior to building your own Docker image. From within your copy of the git repo run the following to retrieve the submodules and build the Docker image:

+
git submodule update --init --recursive
+docker build --tag <YOUR CUSTOM IMAGE NAME> .
+
+

Or, you can clone and retrieve the submodules in one command:

+
git clone --recurse-submodules https://github.com/docker-mailserver/docker-mailserver
+
+

About Docker

+

Minimum supported version

+

We make use of build features that require a recent version of Docker. v23.0 or newer is advised, but earlier releases may work.

+ +

Build Arguments (Optional)

+

The Dockerfile includes several build ARG instructions that can be configured:

+
    +
  • DOVECOT_COMMUNITY_REPO: Install Dovecot from the community repo instead of from Debian (default = 1)
  • +
  • DMS_RELEASE: The image version (default = edge)
  • +
  • VCS_REVISION: The git commit hash used for the build (default = unknown)
  • +
+
+

Note

+
    +
  • DMS_RELEASE (when not edge) will be used to check for updates from our GH releases page at runtime due to the default feature ENABLE_UPDATE_CHECK=1.
  • +
  • Both DMS_RELEASE and VCS_REVISION are also used with opencontainers metadata LABEL instructions.
  • +
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/examples/tutorials/mailserver-behind-proxy/index.html b/v13.1/examples/tutorials/mailserver-behind-proxy/index.html new file mode 100644 index 00000000..1c62f19c --- /dev/null +++ b/v13.1/examples/tutorials/mailserver-behind-proxy/index.html @@ -0,0 +1,2145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tutorials | Mail Server behind a Proxy - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Mailserver behind Proxy

+ +

Using DMS 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 achieved the same goal using different proxy software below:

+
+Traefik v2 +

Truncated configuration of traefik itself:

+
services:
+  reverse-proxy:
+    image: docker.io/traefik:latest # v2.5
+    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 necessary labels on the DMS container:

+
services:
+  mailserver:
+    image: ghcr.io/docker-mailserver/docker-mailserver:latest
+    container_name: mailserver
+    hostname: mail.example.com
+    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.tls.passthrough=true"
+      - "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.routers.imap-ssl.tls.passthrough=true"
+      - "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 necessary to use port 10993 here. More information below at dovecot configuration.

+
+

Configuration of the Backend (dovecot and postfix)

+

The following changes can be achieved 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 docker-data/dms/config/postfix-main.cf:

+
postscreen_upstream_proxy_protocol = haproxy
+
+

and to docker-data/dms/config/postfix-master.cf:

+
submission/inet/smtpd_upstream_proxy_protocol=haproxy
+submissions/inet/smtpd_upstream_proxy_protocol=haproxy
+
+

Changes for dovecot can be applied by adding the following content to docker-data/dms/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/v13.1/examples/use-cases/auth-lua/index.html b/v13.1/examples/use-cases/auth-lua/index.html new file mode 100644 index 00000000..01d8d648 --- /dev/null +++ b/v13.1/examples/use-cases/auth-lua/index.html @@ -0,0 +1,2166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Examples | Use Cases | Lua Authentication - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Lua Authentication

+ +

Introduction

+

Dovecot has the ability to let users create their own custom user provisioning and authentication providers in Lua. This allows any data source that can be approached from Lua to be used for authentication, including web servers. It is possible to do more with Dovecot and Lua, but other use cases fall outside of the scope of this documentation page.

+
+

Community contributed guide

+

Dovecot authentication via Lua scripting is not officially supported in DMS. No assistance will be provided should you encounter any issues.

+

DMS provides the required packages to support this guide. Note that these packages will be removed should they introduce any future maintenance burden.

+

The example in this guide relies on the current way in which DMS works with Dovecot configuration files. Changes to this to accommodate new authentication methods such as OpenID Connect will likely break this example in the future. This guide is updated on a best-effort base.

+
+

Dovecot's Lua support can be used for user provisioning (userdb functionality) and/or password verification (passdb functionality). Consider using other userdb and passdb options before considering Lua, since Lua does require the use of additional (unsupported) program code that might require maintenance when updating DMS.

+

Each implementation of Lua-based authentication is custom. Therefore it is impossible to write documentation that covers every scenario. Instead, this page describes a single example scenario. If that scenario is followed, you will learn vital aspects that are necessary to kickstart your own Lua development:

+
    +
  • How to override Dovecot's default configuration to disable parts that conflict with your scenario.
  • +
  • How to make Dovecot use your Lua script.
  • +
  • How to add your own Lua script and any libraries it uses.
  • +
  • How to debug your Lua script.
  • +
+

The example scenario

+

This scenario starts with DMS being configured to use LDAP for mailbox identification, user authorization and user authentication. In this scenario, Nextcloud is also a service that uses the same LDAP server for user identification, authorization and authentication.

+

The goal of this scenario is to have Dovecot not authenticate the user against LDAP, but against Nextcloud using an application password. The idea behind this is that a compromised mailbox password does not compromise the user's account entirely. To make this work, Nextcloud is configured to deny the use of account passwords by clients and to disable account password reset through mail verification.

+

If the application password is configured correctly, an adversary can only use it to access the user's mailbox on DMS, and CalDAV and CardDAV data on Nextcloud. File access through WebDAV can be disabled for the application password used to access mail. Having CalDAV and CardDAV compromised by the same password is a minor setback. If an adversary gets access to a Nextcloud application password through a device of the user, it is likely that the adversary also gets access to the user's calendars and contact lists anyway (locally or through the same account settings used for mail and CalDAV/CardDAV synchronization). The user's stored files in Nextcloud, the LDAP account password and any other services that rely on it would still be protected. A bonus is that a user is able to revoke and renew the mailbox password in Nextcloud for whatever reason, through a friendly user interface with all the security measures with which the Nextcloud instance is configured (e.g. verification of the current account password).

+

A drawback of this method is that any (compromised) Nextcloud application password can be used to access the user's mailbox. This introduces a risk that a Nextcloud application password used for something else (e.g. WebDAV file access) is compromised and used to access the user's mailbox. Discussion of that risk and possible mitigations fall outside of the scope of this scenario.

+

To answer the questions asked earlier for this specific scenario:

+
    +
  1. Do I want to use Lua to identify mailboxes and verify that users are are authorized to use mail services? No. Provisioning is done through LDAP.
  2. +
  3. Do I want to use Lua to verify passwords that users authenticate with for IMAP/POP3/SMTP in their mail clients? Yes. Password authentication is done through Lua against Nextcloud.
  4. +
  5. If the answer is 'yes' to question 1 or 2: are there other methods that better facilitate my use case instead of custom scripts which rely on me being a developer and not just a user? No. Only HTTP can be used to authenticate against Nextcloud, which is not supported natively by Dovecot or DMS.
  6. +
+

While it is possible to extend the authentication methods which Nextcloud can facilitate with Nextcloud apps, there is currently a mismatch between what DMS supports and what Nextcloud applications can provide. This might change in the future. For now, Lua will be used to bridge the gap between DMS and Nextcloud for authentication only (Dovecot passdb), while LDAP will still be used to identify mailboxes and verify authorization (Dovecot userdb).

+

Modify Dovecot's configuration

+
+Add to DMS volumes in compose.yaml +
    # All new volumes are marked :ro to configure them as read-only, since their contents are not changed from inside the container
+    volumes:
+      # Configuration override to disable LDAP authentication
+      - ./docker-data/dms/config/dovecot/auth-ldap.conf.ext:/etc/dovecot/conf.d/auth-ldap.conf.ext:ro
+      # Configuration addition to enable Lua authentication
+      - ./docker-data/dms/config/dovecot/auth-lua-httpbasic.conf:/etc/dovecot/conf.d/auth-lua-httpbasic.conf:ro
+      # Directory containing Lua scripts
+      - ./docker-data/dms/config/dovecot/lua/:/etc/dovecot/lua/:ro
+
+
+

Create a directory for Lua scripts: +

mkdir -p ./docker-data/dms/config/dovecot/lua
+

+

Create configuration file ./docker-data/dms/config/dovecot/auth-ldap.conf.ext for LDAP user provisioning: +

userdb {
+  driver = ldap
+  args = /etc/dovecot/dovecot-ldap.conf.ext
+}
+

+

Create configuration file ./docker-data/dms/config/dovecot/auth-lua-httpbasic.conf for Lua user authentication: +

passdb {
+  driver = lua
+  args = file=/etc/dovecot/lua/auth-httpbasic.lua blocking=yes
+}
+

+

That is all for configuring Dovecot.

+

Create the Lua script

+

Create Lua file ./docker-data/dms/config/dovecot/lua/auth-httpbasic.lua with contents:

+
local http_url = "https://nextcloud.example.com/remote.php/dav/"
+local http_method = "PROPFIND"
+local http_status_ok = 207
+local http_status_failure = 401
+local http_header_forwarded_for = "X-Forwarded-For"
+
+package.path = package.path .. ";/etc/dovecot/lua/?.lua"
+local base64 = require("base64")
+
+local http_client = dovecot.http.client {
+  timeout = 1000;
+  max_attempts = 1;
+  debug = false;
+}
+
+function script_init()
+  return 0
+end
+
+function script_deinit()
+end
+
+function auth_passdb_lookup(req)
+  local auth_request = http_client:request {
+    url = http_url;
+    method = http_method;
+  }
+  auth_request:add_header("Authorization", "Basic " .. (base64.encode(req.user .. ":" .. req.password)))
+  auth_request:add_header(http_header_forwarded_for, req.remote_ip)
+  local auth_response = auth_request:submit()
+  local resp_status = auth_response:status()
+  local reason = auth_response:reason()
+
+  local returnStatus = dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE
+  local returnDesc = http_method .. " - " .. http_url .. " - " .. resp_status .. " " .. reason
+  if resp_status == http_status_ok
+  then
+    returnStatus = dovecot.auth.PASSDB_RESULT_OK
+    returnDesc = "nopassword=y"
+  elseif resp_status == http_status_failure
+  then
+    returnStatus = dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH
+    returnDesc = ""
+  end
+  return returnStatus, returnDesc
+end
+
+

Replace the hostname in the URL to the actual hostname of Nextcloud.

+

Dovecot provides an HTTP client for use in Lua. Aside of that, Lua by itself is pretty barebones. It chooses library compactness over included functionality. You can see that in that a separate library is referenced to add support for Base64 encoding, which is required for HTTP basic access authentication. This library (also a Lua script) is not included. It must be downloaded and stored in the same directory:

+
cd ./docker-data/dms/config/dovecot/lua
+curl -JLO https://raw.githubusercontent.com/iskolbin/lbase64/master/base64.lua
+
+

Only use native (pure Lua) libraries as dependencies if possible, such as base64.lua from the example. This ensures maximum compatibility. Performance is less of an issue since Lua scripts written for Dovecot probably won't be long or complex, and there won't be a lot of data processing by Lua itself.

+

Debugging a Lua script

+

To see which Lua version is used by Dovecot if you plan to do something that is version dependent, run:

+
docker exec CONTAINER_NAME strings /usr/lib/dovecot/libdovecot-lua.so|grep '^LUA_'
+
+

While Dovecot logs the status of authentication attempts for any passdb backend, Dovecot will also log Lua scripting errors and messages sent to Dovecot's Lua API log functions. The combined DMS log (including that of Dovecot) can be viewed using docker logs CONTAINER_NAME. If the log is too noisy (due to other processes in the container also logging to it), docker exec CONTAINER_NAME cat /var/log/mail/mail.log can be used to view the log of Dovecot and Postfix specifically.

+

If working with HTTP in Lua, setting debug = true; when initiating dovecot.http.client will create debug log messages for every HTTP request and response.

+

Note that Lua runs compiled bytecode, and that scripts will be compiled when they are initially started. Once compiled, the bytecode is cached and changes in the Lua script will not be processed automatically. Dovecot will reload its configuration and clear its cached Lua bytecode when running docker exec CONTAINER_NAME dovecot reload. A (changed) Lua script will be compiled to bytecode the next time it is executed after running the Dovecot reload command.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/examples/use-cases/forward-only-mailserver-with-ldap-authentication/index.html b/v13.1/examples/use-cases/forward-only-mailserver-with-ldap-authentication/index.html new file mode 100644 index 00000000..2665e02d --- /dev/null +++ b/v13.1/examples/use-cases/forward-only-mailserver-with-ldap-authentication/index.html @@ -0,0 +1,2087 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use Cases | Forward-Only Mail Server with LDAP - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Forward-Only Mail-Server with LDAP

+ +

Building a Forward-Only Mail Server

+

A forward-only mail server 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 DMS 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 mail server 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:

+
ACCOUNT_PROVISIONER=LDAP
+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: external-account@gmail.com
+
+

This structure is different from what is expected/assumed from the configuration scripts of DMS, so it doesn't work just by using the LDAP_QUERY_FILTER_... settings. Instead, I had to use a custom configuration (via user-patches.sh). I created the script docker-data/dms/config/user-patches.sh, with 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.

+
+

See also

+

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 mail server is this.

+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/examples/use-cases/imap-folders/index.html b/v13.1/examples/use-cases/imap-folders/index.html new file mode 100644 index 00000000..a1024b0d --- /dev/null +++ b/v13.1/examples/use-cases/imap-folders/index.html @@ -0,0 +1,2128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use Cases | Customize Mailbox Folders - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Mailboxes (aka IMAP Folders)

+

INBOX is setup as the private inbox namespace. By default target/dovecot/15-mailboxes.conf configures the special IMAP folders Drafts, Sent, Junk and Trash to be automatically created and subscribed. They are all assigned to the private inbox namespace (which implicitly provides the INBOX folder).

+

These IMAP folders are considered special because they add a "SPECIAL-USE" attribute, which is a standardized way to communicate to mail clients that the folder serves a purpose like storing spam/junk mail (\Junk) or deleted mail (\Trash). This differentiates them from regular mail folders that you may use for organizing.

+

Adding a mailbox folder

+

See target/dovecot/15-mailboxes.conf for existing mailbox folders which you can modify or uncomment to enable some other common mailboxes. For more information try the official Dovecot documentation.

+

The Archive special IMAP folder may be useful to enable. To do so, make a copy of target/dovecot/15-mailboxes.conf and uncomment the Archive mailbox definition. Mail clients should understand that this folder is intended for archiving mail due to the \Archive "SPECIAL-USE" attribute.

+

With the provided compose.yaml example, a volume bind mounts the host directory docker-data/dms/config/ to the container location /tmp/docker-mailserver/. Config file overrides should instead be mounted to a different location as described in Overriding Configuration for Dovecot:

+
volumes:
+  - ./docker-data/dms/config/dovecot/15-mailboxes.conf:/etc/dovecot/conf.d/15-mailboxes.conf:ro
+
+

Caution

+

Adding folders to an existing setup

+

Handling of newly added mailbox folders can be inconsistent across mail clients:

+
    +
  • Users may experience issues such as archived emails only being available locally.
  • +
  • Users may need to migrate emails manually between two folders.
  • +
+

Support for SPECIAL-USE attributes

+

Not all mail clients support the SPECIAL-USE attribute for mailboxes (defined in RFC 6154). These clients will treat the mailbox folder as any other, using the name assigned to it instead.

+

Some clients may still know to treat these folders for their intended purpose if the mailbox name matches the common names that the SPECIAL-USE attributes represent (eg Sent as the mailbox name for \Sent).

+

Internationalization (i18n)

+

Usually the mail client will know via context such as the SPECIAL-USE attribute or common English mailbox names, to provide a localized label for the users preferred language.

+

Take care to test localized names work well as well.

+

Email Clients Support

+
    +
  • If a new mail account is added without the SPECIAL-USE attribute enabled for archives:
      +
    • Thunderbird suggests and may create an Archives folder on the server.
    • +
    • Outlook for Android archives to a local folder.
    • +
    • Spark for Android archives to server folder named Archive.
    • +
    +
  • +
  • If a new mail account is added after the SPECIAL-USE attribute is enabled for archives:
      +
    • Thunderbird, Outlook for Android and Spark for Android will use the mailbox folder name assigned.
    • +
    +
  • +
+
+

Windows Mail

+

Windows Mail has been said to ignore SPECIAL-USE attribute and look only at the mailbox folder name assigned.

+
+
+

Needs citation

+

This information is provided by the community.

+

It presently lacks references to confirm the behaviour. If any information is incorrect please let us know! 😄

+
+ + + + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/examples/use-cases/ios-mail-push-support/index.html b/v13.1/examples/use-cases/ios-mail-push-support/index.html new file mode 100644 index 00000000..38b20374 --- /dev/null +++ b/v13.1/examples/use-cases/ios-mail-push-support/index.html @@ -0,0 +1,2283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Advanced | iOS Mail Push Support - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

iOS Mail Push Support

+ +

Introduction

+

iOS Mail currently does not support the IMAP idle extension. Therefore users can only either check manually or configure intervals for fetching mails in their mail account preferences when using the default configuration.

+

To support mail push Dovecot needs to advertise the XAPPLEPUSHSERVICE IMAP extension as well as sending the actual push notifications to the Apple Push Notification service (APNs) which will forward them to the device.

+

This can be done with two components:

+
    +
  • A Dovecot plugin (dovecot-xaps-plugin) which is triggered whenever a mail is created or moved from/to a mail folder.
  • +
  • A daemon service (dovecot-xaps-daemon) that manages both the device registrations as well as sending notifications to the APNs.
  • +
+

Prerequisites

+
    +
  • An Apple developer account to create the required Apple Push Notification service certificate.
  • +
  • Knowledge creating Docker images, using the command-line, and creating shell scripts.
  • +
+

Limitations

+
    +
  • You need to maintain a custom docker-mailserver image.
  • +
  • Push support is limited to the INBOX folder. Changes to other folders will not be pushed to the device regardless of the configuration settings.
  • +
  • You currently cannot use the same account UUID on multiple devices. This means that if you use the same backup on multiple devices (e.g. old phone / new phone) only one of them will get the notification. Use different backups or recreate the mail account.
  • +
+

Privacy concerns

+
    +
  • The service does not send any part of the actual message to Apple.
  • +
  • The information sent contains the device UUID to notify and the (on-device) account UUID which was generated by the iOS mail application when creating the account.
  • +
  • Upon receiving the notification, the iOS mail application will connect to the IMAP server given by the provided account UUID and fetch the mail to notify the user.
  • +
  • Apple therefore does not know the mail address for which the mail was received, only that a specific account on a specific device should be notified that a new mail or that a mail was moved to the INBOX folder.
  • +
+

Installation

+

Both components will be built using Docker and included into a custom docker-mailserver image. Afterwards the required configuration is added to docker-data/dms/config. The registration data is stored in /var/mail-state/lib-xapsd.

+
    +
  1. +

    Create a Dockerfile to build a docker-mailserver image that includes the dovecot-xaps-plugin as well as the dovecot-xaps-daemon. This is required to ensure that the Dovecot plugin is built against the same Dovecot version. The :edge tag is used here, but you might want to use a released version instead.

    +
    FROM mailserver/docker-mailserver:edge AS dovecot-plugin-xaps
    +WORKDIR /tmp/dovecot-xaps-plugin
    +RUN <<EOF
    +    apt-get update
    +    apt-get -y --no-install-recommends install git cmake make build-essential dovecot-dev
    +    git clone --single-branch --depth=1 https://github.com/freswa/dovecot-xaps-plugin.git .
    +    mkdir build && cd build
    +    cmake .. -DCMAKE_BUILD_TYPE=Release
    +    make install
    +EOF
    +
    +# Use an older Go version as Go >= 1.20 causes this issue: https://github.com/freswa/dovecot-xaps-daemon/issues/24#issuecomment-1483876081
    +# Note that the underlying issue are non-standard-compliant Apple http servers which might get fixed at some point
    +FROM golang:1.19-alpine AS dovecot-xaps-daemon
    +ENV GOPROXY=https://proxy.golang.org,direct
    +ENV CGO_ENABLED=0
    +WORKDIR /go/dovecot-xaps-daemon
    +RUN <<EOF
    +    apk add --no-cache --virtual build-dependencies git
    +    git clone --single-branch --depth=1 https://github.com/freswa/dovecot-xaps-daemon .
    +    go build ./cmd/xapsd
    +EOF
    +
    +FROM mailserver/docker-mailserver:edge
    +COPY --from=dovecot-plugin-xaps /usr/lib/dovecot/modules/*_xaps_* /usr/lib/dovecot/modules/
    +COPY --from=dovecot-xaps-daemon /go/dovecot-xaps-daemon/xapsd /usr/bin/xapsd
    +
    +# create a non-root user for the daemon process as well as configuration and run state directories
    +RUN <<EOF
    +    adduser --quiet --system --group --disabled-password --home /var/mail-state/lib-xapsd --no-create-home xapsd
    +    mkdir -p /var/run/xapsd /etc/xapsd
    +EOF
    +
    +
  2. +
  3. +

    Build the new image: +

    docker build -t yourname/docker-mailserver .
    +

    +
  4. +
  5. +

    Modify your compose.yaml to use the newly created image: +

        services:
    +        mailserver:
    +          image: yourname/docker-mailserver:latest
    +

    +
  6. +
  7. +

    Recreate the container: +

    docker compose down
    +docker compose up -d
    +

    +
  8. +
  9. +

    Create a hash of your Apple developer account password using the provided xapsd -pass command: +

    docker exec -it mailserver xapsd -pass
    +

    +
  10. +
  11. +

    Add configuration for both components:

    +
      +
    • +

      Create a folder named xaps in docker-data/dms/config.

      +
    • +
    • +

      Create a file named xapsd.yaml in docker-data/dms/config/xaps.

      +
        +
      • Replace appleId and appleIdHashedPassword with your actual credentials. For reference see also here.
      • +
      • The service will use the provided username/hash combination to automatically request a new certificate from Apple as well as renewing an older certificate if needed.
      • +
      +
      xapsd.yaml
      # set the loglevel to either
      +# trace, debug, error, fatal, info, panic or warn
      +# Default: info
      +loglevel: info
      +
      +# xapsd creates a json file to store the registration persistent on disk.
      +# This sets the location of the file.
      +databaseFile: /var/mail-state/lib-xapsd/database.json
      +
      +# xapsd listens on a socket for http/https requests from the dovecot plugin.
      +# This sets the address and port number of the listen socket.
      +listenAddr: '127.0.0.1'
      +port: 11619
      +
      +# xapsd is able to listen on a HTTPS Socket to allow HTTP/2 to be used
      +# SSL is enabled implicitly when certfile and keyfile exist
      +# !!! only use HTTPS for connection pooling with a proxy e.g. nginx or HaProxy
      +# !!! direct usage with the plugin is discouraged and unsupported
      +tlsCertfile:
      +tlsKeyfile:
      +tlsListenAddr:
      +tlsPort: 11620
      +
      +# Notifications that are not initiated by new messages are not sent immediately for two reasons:
      +# 1. When you move/copy/delete messages you most likely move/copy/delete more messages within a short period of time.
      +# 2. You don't need your mailboxes to synchronize immediately since they are automatically synchronized when opening
      +#    the app
      +# If a new message comes and the move/copy/delete notification is still on hold it will be sent with the notification
      +# for the new message.
      +# This sets the interval to check for delayed messages.
      +checkInterval: 20
      +
      +# Set the time how long notifications for not-new messages should be delayed until they are sent.
      +# Whenever checkInterval runs, it checks if "delay" <= "waiting time" and sends the notification if the expression is
      +# true.
      +delay: 30
      +
      +# To retrieve certificates from Apple, we need to login with a valid Apple ID
      +# The accounts email must be given in cleartext, but the password has to
      +# be hashed before sending it. To not leak working credentials on running servers,
      +# we do not accept the cleartext password here.
      +appleId: foo@example.com
      +
      +# use `xaps -pass` to calculate the hash of the apple id password
      +appleIdHashedPassword: bar
      +
      +
    • +
    • +

      Create a file named 95-xaps.conf in docker-data/dms/config/xaps. For reference see also here. +

      95-xaps.conf
      protocol imap {
      +  mail_plugins = $mail_plugins notify push_notification xaps_push_notification xaps_imap
      +}
      +
      +protocol lda {
      +  mail_plugins = $mail_plugins notify push_notification xaps_push_notification
      +}
      +
      +protocol lmtp {
      +  mail_plugins = $mail_plugins notify push_notification xaps_push_notification
      +}
      +
      +plugin {
      +    # xaps_config contains xaps specific configuration parameters
      +    # url:              protocol, hostname and port under which xapsd listens
      +    # user_lookup: Use if you want to determine the username used for PNs from environment variables provided by
      +    #                   login mechanism. Value is variable name to look up.
      +    # max_retries:      maximum num of retries the http client connects to the xaps daemon
      +    # timeout_msecs     http timeout of the http connection
      +    xaps_config = url=http://127.0.0.1:11619 user_lookup=theattribute max_retries=6 timeout_msecs=5000
      +    push_notification_driver = xaps
      +}
      +

      +
    • +
    • +

      Create a supervisord file named xapsd.conf in docker-data/dms/config/xaps with the following content: +

      xapsd.conf
      [program:xapsd]
      +startsecs=0
      +autostart=false
      +autorestart=true
      +stdout_logfile=/var/log/supervisor/%(program_name)s.log
      +stderr_logfile=/var/log/supervisor/%(program_name)s.log
      +user=xapsd
      +command=/usr/bin/xapsd
      +pidfile=/var/run/xapsd/xapsd.pid
      +

      +
    • +
    • +

      Create or update your user-patches.sh in docker-data/dms/config to move the files to their final location as well as starting the daemon service: +

      user-patches.sh
      #!/bin/bash
      +
      +# Copy the configs to internal locations:
      +cp /tmp/docker-mailserver/xaps/95-xaps.conf /etc/dovecot/conf.d/95-xaps.conf
      +cp /tmp/docker-mailserver/xaps/xapsd.yaml /etc/xapsd/xapsd.yaml
      +cp /tmp/docker-mailserver/xaps/xapsd.conf /etc/supervisor/conf.d/xapsd.conf
      +
      +# Setup data persistence and ensure ownership is always for xapsd:
      +mkdir -p /var/mail-state/lib-xapsd
      +chown -R xapsd:xapsd /var/mail-state/lib-xapsd
      +
      +# Start the xaps daemon:
      +supervisorctl update
      +supervisorctl start xapsd
      +

      +
    • +
    +
  12. +
  13. +

    Recreate the container again to apply the new configuration: +

    docker compose down
    +docker compose up -d
    +

    +
  14. +
  15. +

    Recreate your mail account on your iOS device and check the logs in /var/log/supervisor/dovecot.log and /var/log/supervisor/xapsd.log for any errors.

    +
  16. +
+

Other configuration options

+

Both device registration and notifications send a username to the daemon to lookup the device. While the registration and other IMAP operations in Dovecot will send the Dovecot username, LMTP will send the provided authentication username.

+

The format of that username is specified by the auth_username_format Dovecot setting. If you are not using mail addresses as Dovecot usernames - e.g. when using LDAP - you can either change the auth_username_format or add the mail address as property to the user account and use the lookup feature (see below).

+
user-patches.sh
sed -i -r "s|^#?(auth_username_format =).*|\1 %Ln|" /etc/dovecot/conf.d/10-auth.conf
+
+

You can also use notifications for Dovecot alias mailboxes. Depending on your server configuration, this might require to add the original Dovecot username as Dovecot attribute to the login user as well as changing the user_lookup=theattribute in 95-xaps.conf to perform the lookup of that attribute.

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

FAQ

+ +

What kind of database are you using?

+

None! No database is required. The filesystem is the database. This image is based on config files that can be persisted using bind mounts (default) or 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).

+

How are IMAP mailboxes (aka IMAP Folders) set up?

+

INBOX is setup by default with the special IMAP folders Drafts, Sent, Junk and Trash. You can learn how to modify or add your own folders (including additional special folders like Archive) by visiting our docs page Customizing IMAP Folders for more information.

+

How do I update DMS?

+

Make sure to read the CHANGELOG before updating to new versions, to be prepared for possible breaking changes.

+

Then, run the following commands:

+
docker compose pull
+docker compose down
+docker compose up -d
+
+

You should see the new version number on startup, for example: [ INF ] Welcome to docker-mailserver 11.3.1. And you're done! Don't forget to have a look at the remaining functions of the setup.sh script with ./setup.sh help.

+

Which operating systems are supported?

+
    +
  • Linux is officially supported.
  • +
  • Windows and macOS are not supported and users and have reported various issues running the image on these hosts.
  • +
+

As you'll realistically be deploying to production on a Linux host, if you are on Windows or macOS and want to run the image locally first, it's advised to do so via a VM guest running Linux if you have issues running DMS on your host system.

+

What are the system requirements?

+ +
    +
  • 1 vCore
  • +
  • 2GB RAM
  • +
  • Swap enabled for the container
  • +
+

Minimum

+
    +
  • 1 vCore
  • +
  • 512MB RAM
  • +
  • You'll need to avoid running some services like ClamAV (disabled by default) to be able to run on a host with 512MB 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.

+
+

How to alter a running DMS instance without relaunching the container?

+

DMS 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 (these docs use: ./docker-data/dms/config/:/tmp/docker-mailserver/), then restart the sub-service to apply your changes, using supervisorctl. For instance, after editing fail2ban's config: supervisorctl restart fail2ban.

+

See the documentation for supervisorctl.

+
+

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 the container and host date/time?

+

Share the host's /etc/localtime with the container, e.g. by using a bind mount:

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

Optionally, you can set the TZ ENV variable; e.g. TZ=Europe/Berlin. Check this list for which values are allowed.

+

What is the file format?

+

All files are using the Unix format with LF line endings. Please do not use CRLF.

+

Do you support multiple domains?

+

DMS supports multiple domains out of the box, so you can do this:

+
./setup.sh email add user1@example.com
+./setup.sh email add user1@example.de
+./setup.sh email add user1@server.example.org
+
+

What about backups?

+

Bind mounts (default)

+

From the location of your compose.yaml, create a compressed archive of your docker-data/dms/config/ and docker-data/dms/mail-* folders:

+
tar --gzip -cf "backup-$(date +%F).tar.gz" ./docker-data/dms
+
+

Then to restore docker-data/dms/config/ and docker-data/dms/mail-* folders from your backup file:

+
tar --gzip -xf backup-date.tar.gz
+
+

Volumes

+

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

+
# create backup
+docker run --rm -it \
+  -v "${PWD}/docker-data/dms/config/:/tmp/docker-mailserver/" \
+  -v "${PWD}/docker-data/dms-backups/:/backup/" \
+  --volumes-from mailserver \
+  alpine:latest \
+  tar czf "/backup/mail-$(date +%F).tar.gz" /var/mail /var/mail-state /var/log/mail /tmp/docker-mailserver
+
+# delete backups older than 30 days
+find "${PWD}/docker-data/dms-backups/" -type f -mtime +30 -delete
+
+

I Want to Know More About the Ports

+

See this part of the documentation for further details and best practice advice, especially regarding security concerns.

+

How can I configure my email client?

+

Login is full email address (<user>@<domain>).

+
# IMAP
+username:           <user1@example.com>
+password:           <mypassword>
+server:             <mail.example.com>
+imap port:          143 or 993 with STARTTLS/SSL (recommended)
+imap path prefix:   INBOX
+
+# SMTP
+smtp port:          587 or 465 with STARTTLS/SSL (recommended)
+username:           <user1@example.com>
+password:           <mypassword>
+
+

DMS is properly configured for port 587, if possible, we recommend using port 465 for SMTP though. See this section to learn more about ports.

+

Can I use a naked/bare domain (i.e. no hostname)?

+

Yes, but not without some configuration changes. Normally it is assumed that DMS 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 docker-data/dms/config/postfix-main.cf. If that doesn't work, make sure that OVERRIDE_HOSTNAME is blank in your mailserver.env file. Without these changes 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.

+

Also you need to define hostname: example.com in your compose.yaml.

+
+

You might not want a bare domain

+

We encourage you to consider using a subdomain where possible.

+
    +
  • There are benefits to preferring a subdomain.
  • +
  • A bare domain is not required to have user@example.com, that is distinct from your hostname which is identified by a DNS MX record.
  • +
+
+

How can I configure a catch-all?

+

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

+
@example.com user1@example.com
+
+

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

+

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

+
devnull: /dev/null
+
+

Considering you want to delete all the e-mails received for baduser@example.com, add the following line to docker-data/dms/config/postfix-virtual.cf:

+
baduser@example.com devnull
+
+
+

Important

+

If you use a catch-all rule for the main/sub domain, you need another entry in docker-data/dms/config/postfix-virtual.cf:

+
@mail.example.com hello@example.com
+baduser@example.com devnull
+devnull@mail.example.com devnull
+
+
+

What kind of SSL certificates can I use?

+

Both RSA and ECDSA certs are supported. You can provide your own cert files manually, or mount a letsencrypt generated directory (with alternative support for Traefik's acme.json). Check out the SSL_TYPE documentation for more details.

+

I just moved from my old mail server to DMS, 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.example.com. 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.

+

Connection refused or No response at all

+

You see errors like "Connection Refused" and "Connection closed by foreign host", or you cannot connect at all? You may not be able to connect with your mail client (MUA)? Make sure to check Fail2Ban did not ban you (for exceeding the number of tried logins for example)! You can run

+
docker exec <CONTAINER NAME> setup fail2ban
+
+

and check whether your IP address appears. Use

+
docker exec <CONTAINER NAME> setup fail2ban unban <YOUR IP>
+
+

to unban the IP address.

+

How can I authenticate users with SMTP_ONLY=1?

+

See #1247 for an example.

+
+

Todo

+

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

+
+

Common Errors

+

Creating an alias or account with an address for hostname

+

Normally you will assign DMS a hostname such as mail.example.com. If you instead use a bare domain (such as example.com) or add an alias / account with the same value as your hostname, this can cause a conflict for mail addressed to @hostname as Postfix gets confused where to deliver the mail (hostname is configured for only system accounts via the Postfix main.cf setting mydestination).

+

When this conflict is detected you'll find logs similar to this:

+
warning: do not list domain mail.example.com in BOTH mydestination and virtual_mailbox_domains
+...
+NOQUEUE: reject: RCPT from HOST[IP]: 550 5.1.1 <RECIPIENT>: Recipient address rejected: User unknown in local recipient table; ...
+
+

Opt-out of mail being directed to services by excluding $myhostname as a destination with a postfix-main.cf override config:

+
mydestination = localhost.$mydomain, localhost
+
+
+

Tip

+

You may want to configure a postmaster alias via setup alias add to receive system notifications.

+
+
+

Warning

+

Internal mail destined for root, amavis or other accounts will now no longer be received without an alias or account created for them.

+
+

How to use DMS behind a proxy

+

Using user-patches.sh, update the container file /etc/postfix/main.cf to include:

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

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?

+

DMS 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.

+

It is common to use a local directory for config added to docker-mailsever via a volume mount in your compose.yaml (eg: ./docker-data/dms/config/:/tmp/docker-mailserver/).

+

Add or create the script file to your config directory:

+
cd ./docker-data/dms/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.

+

We also have a very similar docs page specifically about this feature!

+
+

Special use-case - patching the supervisord configuration

+

It seems worth noting, that the user-patches.sh gets executed through 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
+
+
+

How to ban custom IP addresses with Fail2ban

+

Use the following command:

+
./setup.sh fail2ban ban <IP>
+
+

The default bantime is 180 days. This value can be customized.

+

What to do in case of SPF/Forwarding problems

+

If you got any problems with SPF and/or forwarding mails, give SRS a try. You enable SRS by setting ENABLE_SRS=1. See the variable description for further information.

+

Why are my emails not being delivered?

+

There are many reasons why email might be rejected, common causes are:

+
    +
  • Wrong or untrustworthy SSL certificate.
  • +
  • A TLD (your domain) or IP address with a bad reputation.
  • +
  • Misconfigured DNS records.
  • +
+

DMS does not manage those concerns, verify they are not causing your delivery problems before reporting a bug on our issue tracker. Resources that can help you troubleshoot:

+
    +
  • mail-tester can test your deliverability.
  • +
  • helloinbox provides a checklist of things to improve your deliverability.
  • +
+

Special Directories

+

What About the docker-data/dms/config/ Directory?

+

This documentation and all example configuration files in the GitHub repository use docker-data/dms/config/ to refer to the directory in the host that is mounted (e.g. via a bind mount) to /tmp/docker-mailserver/ inside the container.

+

Most configuration files for Postfix, Dovecot, etc. are persisted here. Optional configuration is stored here as well.

+

What About the docker-data/dms/mail-state/ Directory?

+

This documentation and all example configuration files in the GitHub repository use docker-data/dms/mail-state/ to refer to the directory in the host that is mounted (e.g. via a bind mount) to /var/mail-state/ inside the container.

+

When you run DMS with the ENV variable ONE_DIR=1 (default), this directory will provide support to persist Fail2Ban blocks, ClamAV signature updates, and the like when the container is restarted or recreated. Service data is relocated to the mail-state folder for the following services: Postfix, Dovecot, Fail2Ban, Amavis, PostGrey, ClamAV, SpamAssassin, Rspamd & Redis.

+

SpamAssasin

+

How can I manage my custom SpamAssassin rules?

+

Antispam rules are managed in docker-data/dms/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 compose.yaml:

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

Why are SpamAssassin x-headers not inserted into my subdomain.example.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 mailserver sa-learn --spam /var/mail/example.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin
+
+

With docker-compose you can more easily use the internal instance of cron within DMS. This is less problematic than the simple solution shown above, because it decouples the learning from the host on which DMS is running, and avoids errors if the mail server is not running.

+

The following configuration works nicely:

+
+Example +

Create a system cron file:

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

Edit the system cron file nano ./docker-data/dms/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`
+#
+# '> /dev/null' to send error notifications from 'stderr' to 'postmaster@example.com'
+#
+# 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/example.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null
+# ham: archive directories
+15 2 * * * root  sa-learn --ham /var/mail/example.com/username/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null
+# ham: inbox subdirectories
+30 2 * * * root  sa-learn --ham /var/mail/example.com/username/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null
+#
+# Everyday 3:00AM, learn spam from all users of a domain
+# spam: junk directory
+0  3 * * * root  sa-learn --spam /var/mail/not-example.com/*/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null
+# ham: archive directories
+15 3 * * * root  sa-learn --ham /var/mail/not-example.com/*/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null
+# ham: inbox subdirectories
+30 3 * * * root  sa-learn --ham /var/mail/not-example.com/*/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null
+
+

Then with compose.yaml:

+
services:
+  mailserver:
+    image: ghcr.io/docker-mailserver/docker-mailserver:latest
+    volumes:
+      - ./docker-data/dms/cron/sa-learn:/etc/cron.d/sa-learn
+
+

Or with Docker Swarm:

+
services:
+  mailserver:
+    image: ghcr.io/docker-mailserver/docker-mailserver:latest
+    # ...
+    configs:
+      - source: my_sa_crontab
+        target: /etc/cron.d/sa-learn
+
+configs:
+  my_sa_crontab:
+    file: ./docker-data/dms/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 do I have more control about what SpamAssassin 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 value 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 docker-data/dms/config/amavis.cf

+
$clean_quarantine_to      = "amavis\@example.com";
+$virus_quarantine_to      = "amavis\@example.com";
+$banned_quarantine_to     = "amavis\@example.com";
+$bad_header_quarantine_to = "amavis\@example.com";
+$spam_quarantine_to       = "amavis\@example.com";
+
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/favicon.ico b/v13.1/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/v13.1/index.html b/v13.1/index.html new file mode 100644 index 00000000..36017f64 --- /dev/null +++ b/v13.1/index.html @@ -0,0 +1,2105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Home - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Welcome to the Documentation for docker-mailserver!

+
+

This Documentation is Versioned

+

Make sure to select the correct version of this documentation! It should match the version of the image you are using. The default version corresponds to the :latest image tag - the most recent stable release.

+
+

This documentation provides you not only with the basic setup and configuration of DMS but also with advanced configuration, elaborate usage scenarios, detailed examples, hints and more.

+

About

+

docker-mailserver, or DMS for short, is a production-ready fullstack but simple mail server (SMTP, IMAP, LDAP, Antispam, Antivirus, etc.). It employs only configuration files, no SQL database. The image is focused around the slogan "Keep it simple and versioned".

+

Contents

+

Getting Started

+

If you're completely new to mail servers or you want to read up on them, check out our Introduction page. If you're new to DMS as a mail server appliance, make sure to read the Usage chapter first. If you want to look at examples for Docker Compose, we have an Examples page.

+

There is also a script - setup.sh - 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.

+

Configuration

+

We have a dedicated configuration page. It contains most of the configuration and explanation you need to setup your mail server properly. Be aware that advanced tasks may still require reading through all parts of this documentation; it may also involve inspecting your running container for debugging purposes. After all, a mail server is a complex arrangement of various programs.

+
+

Important

+

If you'd like to change, patch or alter files or behavior of DMS, you can use a script. Just place a script called user-patches.sh in your ./docker-data/dms/config/ folder volume (which is mounted to /tmp/docker-mailserver/ inside the container) and it will be run on container startup. See the 'Modifications via Script' page for additional documentation and an example.

+
+

You might also want to check out:

+
    +
  1. A list of all configuration options via ENV
  2. +
  3. A list of all optional and automatically created configuration files and directories
  4. +
  5. How to debug your mail server
  6. +
+
+

Tip

+

Definitely check out the FAQ for more information and tips! Please do not open an issue before you have checked our documentation for answers, including the FAQ!

+
+

Tests

+

DMS employs a variety of tests. If you want to know more about our test suite, view our testing docs.

+

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/v13.1/introduction/index.html b/v13.1/introduction/index.html new file mode 100644 index 00000000..585ecddb --- /dev/null +++ b/v13.1/introduction/index.html @@ -0,0 +1,2308 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Introduction - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

An Overview of Mail Server Infrastructure

+

This article answers the question "What is a mail server, and how does it perform its duty?" and it gives the reader an introduction to the field that covers everything you need to know to get started with DMS.

+

The 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).

+

DMS 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.

+

DMS 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 a mail server; while also capable of fetching emails from a mail server 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 FQDN 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, DMS 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 DMS's toolchain fits within the delivery chain:

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

Let's say Alice owns a Gmail account, alice@gmail.com; and Bob owns an account on a DMS 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 DMS instance(MTA); it merely receives the email after it has been relayed by Gmail's MTA. In scenario B, the DMS instance(MTA) handles the submission, prior to relaying.

+

The main takeaway is that when a third-party sends an email to a DMS 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 DMS'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 DMS's toolchain.

+

About Security & Ports

+

Introduction

+

In the previous section, three components were outlined. Each one of those is responsible for a specific task 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 may 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. 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.

+

Overview

+

The following picture gives a visualization of the interplay of all components and their respective ports:

+
  ┏━━━━━━━━━━ Submission ━━━━━━━━━━━━━┓┏━━━━━━━━━━━━━ Transfer/Relay ━━━━━━━━━━━┓
+
+                            ┌─────────────────────┐                    ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
+MUA ----- STARTTLS -------> ┤(587)   MTA ╮    (25)├ <-- cleartext ---> ┊ Third-party MTA ┊
+    ----- implicit TLS ---> ┤(465)       │        |                    └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
+    ----- cleartext ------> ┤(25)        │        |
+                            |┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄|
+MUA <---- STARTTLS -------- ┤(143)   MDA ╯        |
+    <---- implicit 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 DMS'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 DMS, 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.

+

Now let's say I own an account on a DMS 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 DMS 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:

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

Outbound Submission

+

When it comes to securing Outbound Submission you should prefer to use Implicit TLS connection via ESMTP on port 465 (see RFC 8314). Please read our article about Understanding the Ports for more details!

+
+

Warning

+

This Submission setup is sometimes referred 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 a mail server 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 DMS'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.

+
    +
  • DMS's default configuration enables and requires Explicit TLS (STARTTLS) on port 587 for Outbound Submission.
  • +
  • It does not enable Implicit TLS Outbound 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 Outbound 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, DMS uses port 25 for unencrypted Submission in order to support older clients, but most importantly for unencrypted Transfer/Relay between MTAs.

+
    +
  • DMS's default configuration also enables unencrypted (cleartext) on port 25 for Outbound 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 Outbound Submission.
  • +
  • One may also secure Outbound Submission using advanced encryption scheme, such as DANE/DNSSEC and/or MTA-STS.
  • +
+

Inbound 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), Inbound Submission is to be handled in cleartext on port 25 by default.

+
    +
  • DMS's default configuration enables unencrypted (cleartext) on port 25 for Inbound 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 Inbound Submission.
  • +
  • One may also secure Inbound Submission using advanced encryption scheme, such as DANE/DNSSEC and/or MTA-STS.
  • +
+

Overall, DMS's default configuration for SMTP looks like this:

+
  ┏━━━ Outbound Submission ━━━┓
+
+                    ┌────────────────────┐                    ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
+Me -- cleartext --> ┤(25)            (25)├ --- cleartext ---> ┊                 ┊
+Me -- TLS      ---> ┤(465)  My MTA       │                    ┊ Third-party MTA ┊
+Me -- STARTTLS ---> ┤(587)               │                    ┊                 ┊
+                    │                (25)├ <---cleartext ---- ┊                 ┊
+                    └────────────────────┘                    └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
+
+                              ┗━━━━━━━━━━ Inbound 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 DMS, 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.

+

DMS'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).

+

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

+

How Does DMS Help With Setting Everything Up?

+

As a batteries included container image, DMS 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.

+ +

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.

+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v13.1/search/search_index.json b/v13.1/search/search_index.json new file mode 100644 index 00000000..0e8fbef2 --- /dev/null +++ b/v13.1/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Welcome to the Documentation for docker-mailserver!","text":"

This Documentation is Versioned

Make sure to select the correct version of this documentation! It should match the version of the image you are using. The default version corresponds to the :latest image tag - the most recent stable release.

This documentation provides you not only with the basic setup and configuration of DMS but also with advanced configuration, elaborate usage scenarios, detailed examples, hints and more.

"},{"location":"#about","title":"About","text":"

docker-mailserver, or DMS for short, is a production-ready fullstack but simple mail server (SMTP, IMAP, LDAP, Antispam, Antivirus, etc.). It employs only configuration files, no SQL database. The image is focused around the slogan \"Keep it simple and versioned\".

"},{"location":"#contents","title":"Contents","text":""},{"location":"#getting-started","title":"Getting Started","text":"

If you're completely new to mail servers or you want to read up on them, check out our Introduction page. If you're new to DMS as a mail server appliance, make sure to read the Usage chapter first. If you want to look at examples for Docker Compose, we have an Examples page.

There is also a script - setup.sh - 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.

"},{"location":"#configuration","title":"Configuration","text":"

We have a dedicated configuration page. It contains most of the configuration and explanation you need to setup your mail server properly. Be aware that advanced tasks may still require reading through all parts of this documentation; it may also involve inspecting your running container for debugging purposes. After all, a mail server is a complex arrangement of various programs.

Important

If you'd like to change, patch or alter files or behavior of DMS, you can use a script. Just place a script called user-patches.sh in your ./docker-data/dms/config/ folder volume (which is mounted to /tmp/docker-mailserver/ inside the container) and it will be run on container startup. See the 'Modifications via Script' page for additional documentation and an example.

You might also want to check out:

  1. A list of all configuration options via ENV
  2. A list of all optional and automatically created configuration files and directories
  3. How to debug your mail server

Tip

Definitely check out the FAQ for more information and tips! Please do not open an issue before you have checked our documentation for answers, including the FAQ!

"},{"location":"#tests","title":"Tests","text":"

DMS employs a variety of tests. If you want to know more about our test suite, view our testing docs.

"},{"location":"#contributing","title":"Contributing","text":"

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

"},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#what-kind-of-database-are-you-using","title":"What kind of database are you using?","text":"

None! No database is required. The filesystem is the database. This image is based on config files that can be persisted using bind mounts (default) or Docker volumes, and as such versioned, backed up and so forth.

"},{"location":"faq/#where-are-emails-stored","title":"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).

"},{"location":"faq/#how-are-imap-mailboxes-aka-imap-folders-set-up","title":"How are IMAP mailboxes (aka IMAP Folders) set up?","text":"

INBOX is setup by default with the special IMAP folders Drafts, Sent, Junk and Trash. You can learn how to modify or add your own folders (including additional special folders like Archive) by visiting our docs page Customizing IMAP Folders for more information.

"},{"location":"faq/#how-do-i-update-dms","title":"How do I update DMS?","text":"

Make sure to read the CHANGELOG before updating to new versions, to be prepared for possible breaking changes.

Then, run the following commands:

docker compose pull\ndocker compose down\ndocker compose up -d\n

You should see the new version number on startup, for example: [ INF ] Welcome to docker-mailserver 11.3.1. And you're done! Don't forget to have a look at the remaining functions of the setup.sh script with ./setup.sh help.

"},{"location":"faq/#which-operating-systems-are-supported","title":"Which operating systems are supported?","text":"
  • Linux is officially supported.
  • Windows and macOS are not supported and users and have reported various issues running the image on these hosts.

As you'll realistically be deploying to production on a Linux host, if you are on Windows or macOS and want to run the image locally first, it's advised to do so via a VM guest running Linux if you have issues running DMS on your host system.

"},{"location":"faq/#what-are-the-system-requirements","title":"What are the system requirements?","text":""},{"location":"faq/#recommended","title":"Recommended","text":"
  • 1 vCore
  • 2GB RAM
  • Swap enabled for the container
"},{"location":"faq/#minimum","title":"Minimum","text":"
  • 1 vCore
  • 512MB RAM
  • You'll need to avoid running some services like ClamAV (disabled by default) to be able to run on a host with 512MB 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.

"},{"location":"faq/#how-to-alter-a-running-dms-instance-without-relaunching-the-container","title":"How to alter a running DMS instance without relaunching the container?","text":"

DMS 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 (these docs use: ./docker-data/dms/config/:/tmp/docker-mailserver/), then restart the sub-service to apply your changes, using supervisorctl. For instance, after editing fail2ban's config: supervisorctl restart fail2ban.

See the documentation for supervisorctl.

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.

"},{"location":"faq/#how-can-i-sync-the-container-and-host-datetime","title":"How can I sync the container and host date/time?","text":"

Share the host's /etc/localtime with the container, e.g. by using a bind mount:

volumes:\n- /etc/localtime:/etc/localtime:ro\n

Optionally, you can set the TZ ENV variable; e.g. TZ=Europe/Berlin. Check this list for which values are allowed.

"},{"location":"faq/#what-is-the-file-format","title":"What is the file format?","text":"

All files are using the Unix format with LF line endings. Please do not use CRLF.

"},{"location":"faq/#do-you-support-multiple-domains","title":"Do you support multiple domains?","text":"

DMS supports multiple domains out of the box, so you can do this:

./setup.sh email add user1@example.com\n./setup.sh email add user1@example.de\n./setup.sh email add user1@server.example.org\n
"},{"location":"faq/#what-about-backups","title":"What about backups?","text":""},{"location":"faq/#bind-mounts-default","title":"Bind mounts (default)","text":"

From the location of your compose.yaml, create a compressed archive of your docker-data/dms/config/ and docker-data/dms/mail-* folders:

tar --gzip -cf \"backup-$(date +%F).tar.gz\" ./docker-data/dms\n

Then to restore docker-data/dms/config/ and docker-data/dms/mail-* folders from your backup file:

tar --gzip -xf backup-date.tar.gz\n
"},{"location":"faq/#volumes","title":"Volumes","text":"

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

# create backup\ndocker run --rm -it \\\n-v \"${PWD}/docker-data/dms/config/:/tmp/docker-mailserver/\" \\\n-v \"${PWD}/docker-data/dms-backups/:/backup/\" \\\n--volumes-from mailserver \\\nalpine:latest \\\ntar czf \"/backup/mail-$(date +%F).tar.gz\" /var/mail /var/mail-state /var/log/mail /tmp/docker-mailserver\n\n# delete backups older than 30 days\nfind \"${PWD}/docker-data/dms-backups/\" -type f -mtime +30 -delete\n
"},{"location":"faq/#i-want-to-know-more-about-the-ports","title":"I Want to Know More About the Ports","text":"

See this part of the documentation for further details and best practice advice, especially regarding security concerns.

"},{"location":"faq/#how-can-i-configure-my-email-client","title":"How can I configure my email client?","text":"

Login is full email address (<user>@<domain>).

# IMAP\nusername:           <user1@example.com>\npassword:           <mypassword>\nserver:             <mail.example.com>\nimap port:          143 or 993 with STARTTLS/SSL (recommended)\nimap path prefix:   INBOX\n\n# SMTP\nsmtp port:          587 or 465 with STARTTLS/SSL (recommended)\nusername:           <user1@example.com>\npassword:           <mypassword>\n

DMS is properly configured for port 587, if possible, we recommend using port 465 for SMTP though. See this section to learn more about ports.

"},{"location":"faq/#can-i-use-a-nakedbare-domain-ie-no-hostname","title":"Can I use a naked/bare domain (i.e. no hostname)?","text":"

Yes, but not without some configuration changes. Normally it is assumed that DMS 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 docker-data/dms/config/postfix-main.cf. If that doesn't work, make sure that OVERRIDE_HOSTNAME is blank in your mailserver.env file. Without these changes there will be warnings in the logs like:

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

Plus of course mail delivery fails.

Also you need to define hostname: example.com in your compose.yaml.

You might not want a bare domain

We encourage you to consider using a subdomain where possible.

  • There are benefits to preferring a subdomain.
  • A bare domain is not required to have user@example.com, that is distinct from your hostname which is identified by a DNS MX record.
"},{"location":"faq/#how-can-i-configure-a-catch-all","title":"How can I configure a catch-all?","text":"

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

@example.com user1@example.com\n
"},{"location":"faq/#how-can-i-delete-all-the-emails-for-a-specific-user","title":"How can I delete all the emails for a specific user?","text":"

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

devnull: /dev/null\n

Considering you want to delete all the e-mails received for baduser@example.com, add the following line to docker-data/dms/config/postfix-virtual.cf:

baduser@example.com devnull\n

Important

If you use a catch-all rule for the main/sub domain, you need another entry in docker-data/dms/config/postfix-virtual.cf:

@mail.example.com hello@example.com\nbaduser@example.com devnull\ndevnull@mail.example.com devnull\n
"},{"location":"faq/#what-kind-of-ssl-certificates-can-i-use","title":"What kind of SSL certificates can I use?","text":"

Both RSA and ECDSA certs are supported. You can provide your own cert files manually, or mount a letsencrypt generated directory (with alternative support for Traefik's acme.json). Check out the SSL_TYPE documentation for more details.

"},{"location":"faq/#i-just-moved-from-my-old-mail-server-to-dms-but-it-doesnt-work","title":"I just moved from my old mail server to DMS, but \"it doesn't 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.example.com. 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.

"},{"location":"faq/#connection-refused-or-no-response-at-all","title":"Connection refused or No response at all","text":"

You see errors like \"Connection Refused\" and \"Connection closed by foreign host\", or you cannot connect at all? You may not be able to connect with your mail client (MUA)? Make sure to check Fail2Ban did not ban you (for exceeding the number of tried logins for example)! You can run

docker exec <CONTAINER NAME> setup fail2ban\n

and check whether your IP address appears. Use

docker exec <CONTAINER NAME> setup fail2ban unban <YOUR IP>\n

to unban the IP address.

"},{"location":"faq/#how-can-i-authenticate-users-with-smtp_only1","title":"How can I authenticate users with SMTP_ONLY=1?","text":"

See #1247 for an example.

Todo

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

"},{"location":"faq/#common-errors","title":"Common Errors","text":""},{"location":"faq/#creating-an-alias-or-account-with-an-address-for-hostname","title":"Creating an alias or account with an address for hostname","text":"

Normally you will assign DMS a hostname such as mail.example.com. If you instead use a bare domain (such as example.com) or add an alias / account with the same value as your hostname, this can cause a conflict for mail addressed to @hostname as Postfix gets confused where to deliver the mail (hostname is configured for only system accounts via the Postfix main.cf setting mydestination).

When this conflict is detected you'll find logs similar to this:

warning: do not list domain mail.example.com in BOTH mydestination and virtual_mailbox_domains\n...\nNOQUEUE: reject: RCPT from HOST[IP]: 550 5.1.1 <RECIPIENT>: Recipient address rejected: User unknown in local recipient table; ...\n

Opt-out of mail being directed to services by excluding $myhostname as a destination with a postfix-main.cf override config:

mydestination = localhost.$mydomain, localhost\n

Tip

You may want to configure a postmaster alias via setup alias add to receive system notifications.

Warning

Internal mail destined for root, amavis or other accounts will now no longer be received without an alias or account created for them.

"},{"location":"faq/#how-to-use-dms-behind-a-proxy","title":"How to use DMS behind a proxy","text":"

Using user-patches.sh, update the container file /etc/postfix/main.cf to include:

proxy_interfaces = X.X.X.X (your public IP)\n
"},{"location":"faq/#how-to-adjust-settings-with-the-user-patchessh-script","title":"How to adjust settings with the user-patches.sh 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?

DMS 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.

It is common to use a local directory for config added to docker-mailsever via a volume mount in your compose.yaml (eg: ./docker-data/dms/config/:/tmp/docker-mailserver/).

Add or create the script file to your config directory:

cd ./docker-data/dms/config\ntouch user-patches.sh\nchmod +x user-patches.sh\n

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\n./setup.sh debug login\n\n# check the file\ncat /tmp/docker-mailserver/user-patches.sh\n\n# run the script\n/tmp/docker-mailserver/user-patches.sh\n\n# exit the container shell back to the host shell\nexit\n

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.

We also have a very similar docs page specifically about this feature!

Special use-case - patching the supervisord configuration

It seems worth noting, that the user-patches.sh gets executed through 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\nsed -i 's/rimap -r/rimap/' /etc/supervisor/conf.d/saslauth.conf\nsupervisorctl update\n
"},{"location":"faq/#how-to-ban-custom-ip-addresses-with-fail2ban","title":"How to ban custom IP addresses with Fail2ban","text":"

Use the following command:

./setup.sh fail2ban ban <IP>\n

The default bantime is 180 days. This value can be customized.

"},{"location":"faq/#what-to-do-in-case-of-spfforwarding-problems","title":"What to do in case of SPF/Forwarding problems","text":"

If you got any problems with SPF and/or forwarding mails, give SRS a try. You enable SRS by setting ENABLE_SRS=1. See the variable description for further information.

"},{"location":"faq/#why-are-my-emails-not-being-delivered","title":"Why are my emails not being delivered?","text":"

There are many reasons why email might be rejected, common causes are:

  • Wrong or untrustworthy SSL certificate.
  • A TLD (your domain) or IP address with a bad reputation.
  • Misconfigured DNS records.

DMS does not manage those concerns, verify they are not causing your delivery problems before reporting a bug on our issue tracker. Resources that can help you troubleshoot:

  • mail-tester can test your deliverability.
  • helloinbox provides a checklist of things to improve your deliverability.
"},{"location":"faq/#special-directories","title":"Special Directories","text":""},{"location":"faq/#what-about-the-docker-datadmsconfig-directory","title":"What About the docker-data/dms/config/ Directory?","text":"

This documentation and all example configuration files in the GitHub repository use docker-data/dms/config/ to refer to the directory in the host that is mounted (e.g. via a bind mount) to /tmp/docker-mailserver/ inside the container.

Most configuration files for Postfix, Dovecot, etc. are persisted here. Optional configuration is stored here as well.

"},{"location":"faq/#what-about-the-docker-datadmsmail-state-directory","title":"What About the docker-data/dms/mail-state/ Directory?","text":"

This documentation and all example configuration files in the GitHub repository use docker-data/dms/mail-state/ to refer to the directory in the host that is mounted (e.g. via a bind mount) to /var/mail-state/ inside the container.

When you run DMS with the ENV variable ONE_DIR=1 (default), this directory will provide support to persist Fail2Ban blocks, ClamAV signature updates, and the like when the container is restarted or recreated. Service data is relocated to the mail-state folder for the following services: Postfix, Dovecot, Fail2Ban, Amavis, PostGrey, ClamAV, SpamAssassin, Rspamd & Redis.

"},{"location":"faq/#spamassasin","title":"SpamAssasin","text":""},{"location":"faq/#how-can-i-manage-my-custom-spamassassin-rules","title":"How can I manage my custom SpamAssassin rules?","text":"

Antispam rules are managed in docker-data/dms/config/spamassassin-rules.cf.

"},{"location":"faq/#what-are-acceptable-sa_spam_subject-values","title":"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 compose.yaml:

environment:\n- \"SA_SPAM_SUBJECT=[SPAM] \"\n
"},{"location":"faq/#why-are-spamassassin-x-headers-not-inserted-into-my-subdomainexamplecom-subdomain-emails","title":"Why are SpamAssassin x-headers not inserted into my subdomain.example.com 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.

"},{"location":"faq/#how-can-i-make-spamassassin-better-recognize-spam","title":"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`,\n# with a consolidated config in `/var/mail-state`\n#\n# m h dom mon dow command\n# Everyday 2:00AM, learn spam from a specific user\n0 2 * * * docker exec mailserver sa-learn --spam /var/mail/example.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin\n

With docker-compose you can more easily use the internal instance of cron within DMS. This is less problematic than the simple solution shown above, because it decouples the learning from the host on which DMS is running, and avoids errors if the mail server is not running.

The following configuration works nicely:

Example

Create a system cron file:

# in the compose.yaml root directory\nmkdir -p ./docker-data/dms/cron\ntouch ./docker-data/dms/cron/sa-learn\nchown root:root ./docker-data/dms/cron/sa-learn\nchmod 0644 ./docker-data/dms/cron/sa-learn\n

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

# This assumes you're having `environment: ONE_DIR=1` in the env-mailserver,\n# with a consolidated config in `/var/mail-state`\n#\n# '> /dev/null' to send error notifications from 'stderr' to 'postmaster@example.com'\n#\n# m h dom mon dow user command\n#\n# Everyday 2:00AM, learn spam from a specific user\n# spam: junk directory\n0  2 * * * root  sa-learn --spam /var/mail/example.com/username/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: archive directories\n15 2 * * * root  sa-learn --ham /var/mail/example.com/username/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: inbox subdirectories\n30 2 * * * root  sa-learn --ham /var/mail/example.com/username/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n#\n# Everyday 3:00AM, learn spam from all users of a domain\n# spam: junk directory\n0  3 * * * root  sa-learn --spam /var/mail/not-example.com/*/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: archive directories\n15 3 * * * root  sa-learn --ham /var/mail/not-example.com/*/.Archive* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n# ham: inbox subdirectories\n30 3 * * * root  sa-learn --ham /var/mail/not-example.com/*/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin > /dev/null\n

Then with compose.yaml:

services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\nvolumes:\n- ./docker-data/dms/cron/sa-learn:/etc/cron.d/sa-learn\n

Or with Docker Swarm:

services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\n# ...\nconfigs:\n- source: my_sa_crontab\ntarget: /etc/cron.d/sa-learn\n\nconfigs:\nmy_sa_crontab:\nfile: ./docker-data/dms/cron/sa-learn\n

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.

"},{"location":"faq/#how-do-i-have-more-control-about-what-spamassassin-is-filtering","title":"How do I have more control about what SpamAssassin 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\nSA_TAG2=3.75\nSA_KILL=100000.0\n
  • The very negative value 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\n

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\"];\n\nif header :contains \"X-Spam-Flag\" \"YES\" {\n  fileinto \"Junk\";\n} elsif allof (\n  not header :matches \"x-spam-score\" \"-*\",\n  header :value \"ge\" :comparator \"i;ascii-numeric\" \"x-spam-score\" \"3.75\"\n) {\n  fileinto \"Junk\";\n}\n

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

$clean_quarantine_to      = \"amavis\\@example.com\";\n$virus_quarantine_to      = \"amavis\\@example.com\";\n$banned_quarantine_to     = \"amavis\\@example.com\";\n$bad_header_quarantine_to = \"amavis\\@example.com\";\n$spam_quarantine_to       = \"amavis\\@example.com\";\n
"},{"location":"introduction/","title":"An Overview of Mail Server Infrastructure","text":"

This article answers the question \"What is a mail server, and how does it perform its duty?\" and it gives the reader an introduction to the field that covers everything you need to know to get started with DMS.

"},{"location":"introduction/#the-anatomy-of-a-mail-server","title":"The 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).

DMS 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.

DMS 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!

"},{"location":"introduction/#components","title":"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 a mail server; while also capable of fetching emails from a mail server 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 FQDN 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\nFetching an email:   MUA <--------------------------------- MDA\n

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, DMS 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 DMS's toolchain fits within the delivery chain:

                                    docker-mailserver is here:\n                                                        \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\nSending an email:   MUA ---> MTA ---> (MTA relays) ---> \u252b MTA \u256e \u2503\nFetching an email:  MUA <------------------------------ \u252b MDA \u256f \u2503\n                                                        \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
An Example

Let's say Alice owns a Gmail account, alice@gmail.com; and Bob owns an account on a DMS 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 DMS instance(MTA); it merely receives the email after it has been relayed by Gmail's MTA. In scenario B, the DMS instance(MTA) handles the submission, prior to relaying.

The main takeaway is that when a third-party sends an email to a DMS 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 DMS'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 DMS's toolchain.

"},{"location":"introduction/#about-security-ports","title":"About Security & Ports","text":""},{"location":"introduction/#introduction","title":"Introduction","text":"

In the previous section, three components were outlined. Each one of those is responsible for a specific task 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 may 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. 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.

"},{"location":"introduction/#overview","title":"Overview","text":"

The following picture gives a visualization of the interplay of all components and their respective ports:

  \u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Submission \u2501\u2501\u2501\u2501\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\n\n                            \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\nMUA ----- STARTTLS -------> \u2524(587)   MTA \u256e    (25)\u251c <-- cleartext ---> \u250a Third-party MTA \u250a\n    ----- implicit TLS ---> \u2524(465)       \u2502        |                    \u2514\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2518\n    ----- cleartext ------> \u2524(25)        \u2502        |\n                            |\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504|\nMUA <---- STARTTLS -------- \u2524(143)   MDA \u256f        |\n    <---- implicit TLS ---- \u2524(993)                |\n                            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n  \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Retrieval \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n

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 DMS's configuration, including how you can customize it.

"},{"location":"introduction/#submission-smtp","title":"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 DMS, 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.

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

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

In the first scenario, I will be submitting my email directly to my DMS 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:

  • Outbound Submission (self-owned email is submitted directly to the MTA, then is relayed \"outside\")
  • Inbound Submission (third-party email has been submitted & relayed, then is accepted \"inside\" by the MTA)
  \u250f\u2501\u2501\u2501 Outbound Submission \u2501\u2501\u2501\u2513\n\n                    \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\nMe ---------------> \u2524                    \u251c -----------------> \u250a                 \u250a\n                    \u2502       My MTA       \u2502                    \u250a Third-party MTA \u250a\n                    \u2502                    \u251c <----------------- \u250a                 \u250a\n                    \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\n\n                              \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Inbound Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
"},{"location":"introduction/#outbound-submission","title":"Outbound Submission","text":"

When it comes to securing Outbound Submission you should prefer to use Implicit TLS connection via ESMTP on port 465 (see RFC 8314). Please read our article about Understanding the Ports for more details!

Warning

This Submission setup is sometimes referred 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 a mail server 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 DMS'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.

  • DMS's default configuration enables and requires Explicit TLS (STARTTLS) on port 587 for Outbound Submission.
  • It does not enable Implicit TLS Outbound 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 Outbound 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, DMS uses port 25 for unencrypted Submission in order to support older clients, but most importantly for unencrypted Transfer/Relay between MTAs.

  • DMS's default configuration also enables unencrypted (cleartext) on port 25 for Outbound 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 Outbound Submission.
  • One may also secure Outbound Submission using advanced encryption scheme, such as DANE/DNSSEC and/or MTA-STS.
"},{"location":"introduction/#inbound-submission","title":"Inbound 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), Inbound Submission is to be handled in cleartext on port 25 by default.

  • DMS's default configuration enables unencrypted (cleartext) on port 25 for Inbound 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 Inbound Submission.
  • One may also secure Inbound Submission using advanced encryption scheme, such as DANE/DNSSEC and/or MTA-STS.

Overall, DMS's default configuration for SMTP looks like this:

  \u250f\u2501\u2501\u2501 Outbound Submission \u2501\u2501\u2501\u2513\n\n                    \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\nMe -- cleartext --> \u2524(25)            (25)\u251c --- cleartext ---> \u250a                 \u250a\nMe -- TLS      ---> \u2524(465)  My MTA       \u2502                    \u250a Third-party MTA \u250a\nMe -- STARTTLS ---> \u2524(587)               \u2502                    \u250a                 \u250a\n                    \u2502                (25)\u251c <---cleartext ---- \u250a                 \u250a\n                    \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\n\n                              \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Inbound Submission \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251b\n
"},{"location":"introduction/#retrieval-imap","title":"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 DMS, 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.

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

"},{"location":"introduction/#retrieval-pop3","title":"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).

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

"},{"location":"introduction/#how-does-dms-help-with-setting-everything-up","title":"How Does DMS Help With Setting Everything Up?","text":"

As a batteries included container image, DMS 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 DMS's upstream, for a complete control over how things run.

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.

"},{"location":"usage/","title":"Usage","text":"

This pages explains how to get started with DMS. The guide uses Docker Compose as a reference. In our examples, a volume mounts the host location docker-data/dms/config/ to /tmp/docker-mailserver/ inside the container.

"},{"location":"usage/#preliminary-steps","title":"Preliminary Steps","text":"

Before you can get started with deploying your own mail server, there are some requirements to be met:

  1. You need to have a host that you can manage.
  2. You need to own a domain, and you need to able to manage DNS for this domain.
"},{"location":"usage/#host-setup","title":"Host Setup","text":"

There are a few requirements for a suitable host system:

  1. The host should have a static IP address; otherwise you will need to dynamically update DNS (undesirable due to DNS caching)
  2. The host should be able to send/receive on the necessary ports for mail
  3. You should be able to set a PTR record for your host; security-hardened mail servers might otherwise reject your mail server as the IP address of your host does not resolve correctly/at all to the DNS name of your server.

About the Container Runtime

On the host, you need to have a suitable container runtime (like Docker or Podman) installed. We assume Docker Compose is installed. We have aligned file names and configuration conventions with the latest Docker Compose (currently V2) specification.

If you're using podman, make sure to read the related documentation.

"},{"location":"usage/#minimal-dns-setup","title":"Minimal DNS Setup","text":"

The DNS setup is a big and essential part of the whole setup. There is a lot of confusion for newcomers and people starting out when setting up DNS. This section provides an example configuration and supplementary explanation. We expect you to be at least a bit familiar with DNS, what it does and what the individual record types are.

Now let's say you just bought example.com and you want to be able to send and receive e-mails for the address test@example.com. On the most basic level, you will need to

  1. set an MX record for your domain example.com - in our example, the MX record contains mail.example.com
  2. set an A record that resolves the name of your mail server - in our example, the A record contains 11.22.33.44
  3. (in a best-case scenario) set a PTR record that resolves the IP of your mail server - in our example, the PTR contains mail.example.com

We will later dig into DKIM, DMARC & SPF, but for now, these are the records that suffice in getting you up and running. Here is a short explanation of what the records do:

  • The MX record tells everyone which (DNS) name is responsible for e-mails on your domain. Because you want to keep the option of running another service on the domain name itself, you run your mail server on mail.example.com. This does not imply your e-mails will look like test@mail.example.com, the DNS name of your mail server is decoupled of the domain it serves e-mails for. In theory, you mail server could even serve e-mails for test@some-other-domain.com, if the MX record for some-other-domain.com points to mail.example.com.
  • The A record tells everyone which IP address the DNS name mail.example.com resolves to.
  • The PTR record is the counterpart of the A record, telling everyone what name the IP address 11.22.33.44 resolves to.

About The Mail Server's Fully Qualified Domain Name

The mail server's fully qualified domain name (FQDN) in our example above is mail.example.com. Please note though that this is more of a convention, and not due to technical restrictions. One could also run the mail server

  1. on foo.example.com: you would just need to change your MX record;
  2. on example.com directly: you would need to change your MX record and probably read our docs on bare domain setups, as these setups are called \"bare domain\" setups.

The FQDN is what is relevant for TLS certificates, it has no (inherent/technical) relation to the email addresses and accounts DMS manages. That is to say: even though DMS runs on mail.example.com, or foo.example.com, or example.com, there is nothing that prevents it from managing mail for barbaz.org - barbaz.org will just need to set its MX record to mail.example.com (or foo.example.com or example.com).

If you setup everything, it should roughly look like this:

$ dig @1.1.1.1 +short MX example.com\nmail.example.com\n$ dig @1.1.1.1 +short A mail.example.com\n11.22.33.44\n$ dig @1.1.1.1 +short -x 11.22.33.44\nmail.example.com\n
"},{"location":"usage/#deploying-the-actual-image","title":"Deploying the Actual Image","text":""},{"location":"usage/#tagging-convention","title":"Tagging Convention","text":"

To understand which tags you should use, read this section carefully. Our CI will automatically build, test and push new images to the following container registries:

  1. DockerHub (docker.io/mailserver/docker-mailserver)
  2. GitHub Container Registry (ghcr.io/docker-mailserver/docker-mailserver)

All workflows are using the tagging convention listed below. It is subsequently applied to all images.

Event Image Tags push on master edge push a tag (v1.2.3) 1.2.3, 1.2, 1, latest"},{"location":"usage/#get-all-files","title":"Get All Files","text":"

Issue the following commands to acquire the necessary files:

DMS_GITHUB_URL=\"https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master\"\nwget \"${DMS_GITHUB_URL}/compose.yaml\"\nwget \"${DMS_GITHUB_URL}/mailserver.env\"\n
"},{"location":"usage/#configuration-steps","title":"Configuration Steps","text":"
  1. First edit compose.yaml to your liking
    • Substitute mail.example.com according to your FQDN.
    • If you want to use SELinux for the ./docker-data/dms/config/:/tmp/docker-mailserver/ mount, append -z or -Z.
  2. Then configure the environment specific to the mail server by editing mailserver.env, but keep in mind that:
    • only basic VAR=VAL is supported
    • do not quote your values
    • variable substitution is not supported, e.g. OVERRIDE_HOSTNAME=$HOSTNAME.$DOMAINNAME does not work
"},{"location":"usage/#get-up-and-running","title":"Get Up and Running","text":"

Using the Correct Commands For Stopping and Starting DMS

Use docker compose up / down, not docker compose start / stop. Otherwise, the container is not properly destroyed and you may experience problems during startup because of inconsistent state.

Using Ctrl+C is not supported either!

For an overview of commands to manage DMS config, run: docker exec -it <CONTAINER NAME> setup help.

Usage of setup.sh when no DMS Container Is Running

We encourage you to directly use setup inside the container (like shown above). If you still want to use setup.sh, here's some information about it.

If no DMS container is running, any ./setup.sh command will check online for the :latest image tag (the current stable release), performing a docker pull ... if necessary followed by running the command in a temporary container:

$ ./setup.sh help\nImage 'ghcr.io/docker-mailserver/docker-mailserver:latest' not found. Pulling ...\nSETUP(1)\n\nNAME\n    setup - 'docker-mailserver' Administration & Configuration script\n...\n\n$ docker run --rm ghcr.io/docker-mailserver/docker-mailserver:latest setup help\nSETUP(1)\n\nNAME\n    setup - 'docker-mailserver' Administration & Configuration script\n...\n

On first start, you will need to add at least one email account (unless you're using LDAP). You have two minutes to do so, otherwise DMS will shutdown and restart. You can add accounts by running docker exec -ti <CONTAINER NAME> setup email add user@example.com. That's it! It really is that easy.

"},{"location":"usage/#further-miscellaneous-steps","title":"Further Miscellaneous Steps","text":""},{"location":"usage/#setting-up-tls","title":"Setting up TLS","text":"

You definitely want to setup TLS. Please refer to our documentation about TLS.

"},{"location":"usage/#aliases","title":"Aliases","text":"

You should add at least one alias, the postmaster alias. This is a common convention, but not strictly required.

docker exec -ti <CONTAINER NAME> setup alias add postmaster@example.com user@example.com\n
"},{"location":"usage/#advanced-dns-setup-dkim-dmarc-spf","title":"Advanced DNS Setup - DKIM, DMARC & SPF","text":"

You will very likely want to configure your DNS with these TXT records: SPF, DKIM, and DMARC. We also ship a dedicated page in our documentation about the setup of DKIM, DMARC & SPF.

"},{"location":"usage/#custom-user-changes-patches","title":"Custom User Changes & Patches","text":"

If you'd like to change, patch or alter files or behavior of DMS, you can use a script. See this part of our documentation for a detailed explanation.

"},{"location":"usage/#testing","title":"Testing","text":"

Here are some tools you can use to verify your configuration:

  1. MX Toolbox
  2. DMARC Analyzer
  3. mail-tester.com
  4. multiRBL.valli.org
  5. internet.nl
"},{"location":"config/debugging/","title":"Debugging","text":"

This page contains valuable information when it comes to resolving issues you encounter.

Contributions Welcome!

Please consider contributing solutions to the FAQ

"},{"location":"config/debugging/#preliminary-checks","title":"Preliminary Checks","text":"
  • Check that all published DMS ports are actually open and not blocked by your ISP / hosting provider.
  • SSL errors are likely the result of a wrong setup on the user side and not caused by DMS itself.
  • Ensure that you have correctly started DMS. Many problems related to configuration are due to this.

Correctly starting DMS

Use the --force-recreate option to avoid configuration mishaps: docker compose up --force-recreate

Alternatively, always use docker compose down to stop DMS. Do not rely on CTRL + C, docker compose stop, or docker compose restart.

DMS setup scripts are run when a container starts, but may fail to work properly if you do the following:

  • Stopping a container with commands like: docker stop or docker compose up stopped via CTRL + C instead of docker compose down.
  • Restarting a container.

Volumes persist data across container instances, however the same container instance will keep internal changes not stored in a volume until the container is removed.

Due to this, DMS setup scripts may modify configuration it has already modified in the past.

  • This is brittle as some changes are naive by assuming they are applied to the original configs from the image.
  • Volumes in compose.yaml are expected to persist any important data. Thus it should be safe to throwaway the container created each time, avoiding this config problem.
"},{"location":"config/debugging/#mail-sent-from-dms-does-not-arrive-at-destination","title":"Mail sent from DMS does not arrive at destination","text":"

Some service providers block outbound traffic on port 25. Common hosting providers known to have this issue:

  • Azure
  • AWS EC2
  • Vultr

These links may advise how the provider can unblock the port through additional services offered, or via a support ticket request.

"},{"location":"config/debugging/#mail-sent-to-dms-does-not-get-delivered-to-user","title":"Mail sent to DMS does not get delivered to user","text":"

Common logs related to this are:

  • warning: do not list domain domain.fr in BOTH mydestination and virtual_mailbox_domains
  • Recipient address rejected: User unknown in local recipient table

If your logs look like this, you likely have assigned the same FQDN to the DMS hostname and your mail accounts which is not supported by default. You can either adjust your DMS hostname or follow this FAQ advice

It is also possible that DMS services are temporarily unavailable when configuration changes are detected, producing the 2nd error. Certificate updates may be a less obvious trigger.

"},{"location":"config/debugging/#steps-for-debugging-dms","title":"Steps for Debugging DMS","text":"
  1. Increase log verbosity: Very helpful for troubleshooting problems during container startup. Set the environment variable LOG_LEVEL to debug or trace.
  2. Use error logs as a search query: Try finding an existing issue or search engine result from any errors in your container log output. Often you'll find answers or more insights. If you still need to open an issue, sharing links from your search may help us assist you. The mail server log can be acquired by running docker log <CONTAINER NAME> (or docker logs -f <CONTAINER NAME> if you want to follow the log).
  3. Inspect the logs of the service that is failing: We provide a dedicated paragraph on this topic further down below.
  4. Understand the basics of mail servers: Especially for beginners, make sure you read our Introduction and Usage articles.
  5. Search the whole FAQ: Our FAQ contains answers for common problems. Make sure you go through the list.
  6. Reduce the scope: Ensure that you can run a basic setup of DMS first. Then incrementally restore parts of your original configuration until the problem is reproduced again. If you're new to DMS, it is common to find the cause is misunderstanding how to configure a minimal setup.
"},{"location":"config/debugging/#debug-a-running-container","title":"Debug a running container","text":""},{"location":"config/debugging/#general","title":"General","text":"

To get a shell inside the container run: docker exec -it <CONTAINER NAME> bash. To install additional software, run:

  1. apt-get update to update repository metadata.
  2. apt-get install <PACKAGE> to install a package, e.g., apt-get install neovim if you want to use NeoVim instead of nano (which is shipped by default).
"},{"location":"config/debugging/#logs","title":"Logs","text":"

If you need more flexibility than what the docker logs command offers, then the most useful locations to get relevant DMS logs within the container are:

  • /var/log/mail/<SERVICE>.log
  • /var/log/supervisor/<SERVICE>.log

You may use nano (a text editor) to edit files, while less (a file viewer) and tail/cat are useful tools to inspect the contents of logs.

"},{"location":"config/debugging/#compatibility","title":"Compatibility","text":"

It's possible that the issue you're experiencing is due to a compatibility conflict.

This could be from outdated software, or running a system that isn't able to provide you newer software and kernels. You may want to verify if you can reproduce the issue on a system that is not affected by these concerns.

"},{"location":"config/debugging/#network","title":"Network","text":"
  • Misconfigured network connections can cause the client IP address to be proxied through a docker network gateway IP, or a service that acts on behalf of connecting clients for logins where the connections client IP appears to be only from that service (eg: Container IP) instead. This can relay the wrong information to other services (eg: monitoring like Fail2Ban, SPF verification) causing unexpected failures.
  • userland-proxy: Prior to Docker v23, changing the userland-proxy setting did not reliably remove NAT rules.
  • UFW / firewalld: Some users expect only their firewall frontend to manage the firewall rules, but these will be bypassed when Docker publishes a container port (as there is no integration between the two).
  • iptables / nftables:
    • Docker only manages the NAT rules via iptables, relying on compatibility shims for supporting the successor nftables. Internally DMS expects nftables support on the host kernel for services like Fail2Ban to function correctly.
    • Kernels older than 5.2 may affect management of NAT rules via nftables. Other software outside of DMS may also manipulate these rules, such as firewall frontends.
  • IPv6:
    • Requires additional configuration to prevent or properly support IPv6 connections (eg: Preserving the Client IP).
    • Support in 2023 is still considered experimental. You are advised to use at least Docker Engine v23 (2023Q1).
    • Various networking bug fixes have been addressed since the initial IPv6 support arrived in Docker Engine v20.10.0 (2020Q4).
"},{"location":"config/debugging/#system","title":"System","text":"
  • macOS: DMS has limited support for macOS. Often an issue encountered is due to permissions related to the volumes config in compose.yaml. You may have luck trying gRPC FUSE as the file sharing implementation; VirtioFS is the successor but presently appears incompatible with DMS.
  • Kernel: Some systems provide kernels with modifications (replacing defaults and backporting patches) to support running legacy software or kernels, complicating compatibility. This can be commonly experienced with products like NAS.
  • CGroups v2: Hosts running older kernels (prior to 5.2) and systemd (prior to v244) are not likely to leverage cgroup v2, or have not defaulted to the cgroup v2 unified hierarchy. Not meeting this baseline may influence the behaviour of your DMS container, even with the latest Docker Engine installed.
  • Container runtime: Docker and Podman for example have subtle differences. DMS docs are primarily focused on Docker, but we try to document known issues where relevant.
  • Rootless containers: Introduces additional differences in behaviour or requirements:
    • cgroup v2 is required for supporting rootless containers.
    • Differences such as for container networking which may further affect support for IPv6 and preserving the client IP (Remote address). Example with Docker rootless are binding a port to a specific interface and the choice of port forwarding driver.
"},{"location":"config/environment/","title":"Environment Variables","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 ghcr.io/docker-mailserver/docker-mailserver:edge.

"},{"location":"config/environment/#general","title":"General","text":""},{"location":"config/environment/#override_hostname","title":"OVERRIDE_HOSTNAME","text":"

If you can't set your hostname (eg: you're in a container platform that doesn't let you) specify it via this environment variable. It will have priority over docker run --hostname, or the equivalent hostname: field in compose.yaml.

  • empty => Uses the hostname -f command to get canonical hostname for DMS to use.
  • => Specify an FQDN (fully-qualified domain name) to serve mail for. The hostname is required for DMS to function correctly.
"},{"location":"config/environment/#log_level","title":"LOG_LEVEL","text":"

Set the log level for DMS. This is mostly relevant for container startup scripts and change detection event feedback.

Valid values (in order of increasing verbosity) are: error, warn, info, debug and trace. The default log level is info.

"},{"location":"config/environment/#supervisor_loglevel","title":"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.

"},{"location":"config/environment/#dms_vmail_uid","title":"DMS_VMAIL_UID","text":"

Default: 5000

The User ID assigned to the static vmail user for /var/mail (Mail storage managed by Dovecot).

"},{"location":"config/environment/#dms_vmail_gid","title":"DMS_VMAIL_GID","text":"

Default: 5000

The Group ID assigned to the static vmail group for /var/mail (Mail storage managed by Dovecot).

"},{"location":"config/environment/#one_dir","title":"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. See the related FAQ entry for more information.
"},{"location":"config/environment/#account_provisioner","title":"ACCOUNT_PROVISIONER","text":"

Configures the provisioning source of user accounts (including aliases) for user queries and authentication by services managed by DMS (Postfix and Dovecot).

User provisioning via OIDC is planned for the future, see this tracking issue.

  • empty => use FILE
  • LDAP => use LDAP authentication
  • OIDC => use OIDC authentication (not yet implemented)
  • FILE => use local files (this is used as the default)

A second container for the ldap service is necessary (e.g. bitnami/openldap).

"},{"location":"config/environment/#permit_docker","title":"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.

  • none => Explicitly force authentication
  • container => Container IP address 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.

"},{"location":"config/environment/#tz","title":"TZ","text":"

Set the timezone. If this variable is unset, the container runtime will try to detect the time using /etc/localtime, which you can alternatively mount into the container. The value of this variable must follow the pattern AREA/ZONE, i.e. of you want to use Germany's time zone, use Europe/Berlin. You can lookup all available timezones here.

"},{"location":"config/environment/#enable_amavis","title":"ENABLE_AMAVIS","text":"

Amavis content filter (used for ClamAV & SpamAssassin)

  • 0 => Amavis is disabled
  • 1 => Amavis is enabled
"},{"location":"config/environment/#amavis_loglevel","title":"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)
"},{"location":"config/environment/#enable_dnsbl","title":"ENABLE_DNSBL","text":"

This enables DNS block lists in Postscreen. If you want to know which lists we are using, have a look at the default main.cf for Postfix we provide and search for postscreen_dnsbl_sites.

A Warning On DNS Block Lists

Make sure your DNS queries are properly resolved, i.e. you will most likely not want to use a public DNS resolver as these queries do not return meaningful results. We try our best to only evaluate proper return codes - this is not a guarantee that all codes are handled fine though.

Note that emails will be rejected if they don't pass the block list checks!

  • 0 => DNS block lists are disabled
  • 1 => DNS block lists are enabled
"},{"location":"config/environment/#enable_opendkim","title":"ENABLE_OPENDKIM","text":"

Enables the OpenDKIM service.

  • 1 => Enabled
  • 0 => Disabled
"},{"location":"config/environment/#enable_opendmarc","title":"ENABLE_OPENDMARC","text":"

Enables the OpenDMARC service.

  • 1 => Enabled
  • 0 => Disabled
"},{"location":"config/environment/#enable_policyd_spf","title":"ENABLE_POLICYD_SPF","text":"

Enabled policyd-spf in Postfix's configuration. You will likely want to set this to 0 in case you're using Rspamd (ENABLE_RSPAMD=1).

  • 0 => Disabled
  • 1 => Enabled
"},{"location":"config/environment/#enable_pop3","title":"ENABLE_POP3","text":"
  • 0 => POP3 service disabled
  • 1 => Enables POP3 service
"},{"location":"config/environment/#enable_imap","title":"ENABLE_IMAP","text":"
  • 0 => Disabled
  • 1 => Enabled
"},{"location":"config/environment/#enable_clamav","title":"ENABLE_CLAMAV","text":"
  • 0 => ClamAV is disabled
  • 1 => ClamAV is enabled
"},{"location":"config/environment/#enable_fail2ban","title":"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 compose.yaml:

cap_add:\n  - NET_ADMIN\n

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

"},{"location":"config/environment/#fail2ban_blocktype","title":"FAIL2BAN_BLOCKTYPE","text":"
  • drop => drop packet (send NO reply)
  • reject => reject packet (send ICMP unreachable) FAIL2BAN_BLOCKTYPE=drop
"},{"location":"config/environment/#smtp_only","title":"SMTP_ONLY","text":"
  • empty => all daemons start
  • 1 => only launch postfix smtp
"},{"location":"config/environment/#ssl_type","title":"SSL_TYPE","text":"

In the majority of cases, you want letsencrypt or manual.

self-signed can be used for testing SSL until you provide a valid certificate, note that third-parties cannot trust self-signed certificates, do not use this type in production. custom is a temporary workaround that is not officially supported.

  • empty => SSL disabled.
  • letsencrypt => Support for using certificates with Let's Encrypt provisioners. (Docs: Let's Encrypt Setup)
  • manual => Provide your own certificate via separate key and cert files. (Docs: Bring Your Own Certificates)
    • 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.
  • custom => Provide your own certificate as a single file containing both the private key and full certificate chain. (Docs: None)
  • self-signed => Provide your own self-signed certificate files. Expects a self-signed CA cert for verification. Use only for local testing of your setup. (Docs: Self-Signed Certificates)

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

"},{"location":"config/environment/#tls_level","title":"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.
"},{"location":"config/environment/#spoof_protection","title":"SPOOF_PROTECTION","text":"

Configures the handling of creating mails with forged sender addresses.

  • 0 => (not recommended) Mail address spoofing allowed. Any logged in user may create email messages with a forged sender address.
  • 1 => 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.
"},{"location":"config/environment/#enable_srs","title":"ENABLE_SRS","text":"

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

  • 0 => Disabled
  • 1 => Enabled
"},{"location":"config/environment/#network_interface","title":"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
"},{"location":"config/environment/#virusmails_delete_delay","title":"VIRUSMAILS_DELETE_DELAY","text":"

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

  • empty => 7 days
"},{"location":"config/environment/#postfix_dagent","title":"POSTFIX_DAGENT","text":"

Configure Postfix virtual_transport to deliver mail to a different LMTP client (default is a unix socket to dovecot).

Provide any valid URI. Examples:

  • empty => lmtp:unix:/var/run/dovecot/lmtp (default, configured in Postfix main.cf)
  • lmtp:unix:private/dovecot-lmtp (use socket)
  • lmtps:inet:<host>:<port> (secure lmtp with starttls)
  • lmtp:<kopano-host>:2003 (use kopano as mailstore)
"},{"location":"config/environment/#postfix_mailbox_size_limit","title":"POSTFIX_MAILBOX_SIZE_LIMIT","text":"

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

  • empty => 0 (no limit)
"},{"location":"config/environment/#enable_quotas","title":"ENABLE_QUOTAS","text":"
  • 1 => Dovecot quota is enabled
  • 0 => Dovecot quota is disabled

See mailbox quota.

"},{"location":"config/environment/#postfix_message_size_limit","title":"POSTFIX_MESSAGE_SIZE_LIMIT","text":"

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

  • empty => 10240000 (~10 MB)
"},{"location":"config/environment/#clamav_message_size_limit","title":"CLAMAV_MESSAGE_SIZE_LIMIT","text":"

Mails larger than this limit won't be scanned. ClamAV must be enabled (ENABLE_CLAMAV=1) for this.

  • empty => 25M (25 MB)
"},{"location":"config/environment/#enable_managesieve","title":"ENABLE_MANAGESIEVE","text":"
  • empty => Managesieve service disabled
  • 1 => Enables Managesieve on port 4190
"},{"location":"config/environment/#postmaster_address","title":"POSTMASTER_ADDRESS","text":"
  • empty => postmaster@example.com
  • => Specify the postmaster address
"},{"location":"config/environment/#enable_update_check","title":"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
"},{"location":"config/environment/#update_check_interval","title":"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
"},{"location":"config/environment/#postscreen_action","title":"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.
"},{"location":"config/environment/#dovecot_mailbox_format","title":"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.

"},{"location":"config/environment/#postfix_reject_unknown_client_hostname","title":"POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME","text":"

If enabled, employs reject_unknown_client_hostname to sender restrictions in Postfix's configuration.

  • 0 => Disabled
  • 1 => Enabled
"},{"location":"config/environment/#postfix_inet_protocols","title":"POSTFIX_INET_PROTOCOLS","text":"
  • all => Listen on all interfaces.
  • ipv4 => Listen only on IPv4 interfaces. Most likely you want this behind Docker.
  • ipv6 => Listen only on IPv6 interfaces.

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

"},{"location":"config/environment/#dovecot_inet_protocols","title":"DOVECOT_INET_PROTOCOLS","text":"
  • all => Listen on all interfaces
  • ipv4 => Listen only on IPv4 interfaces. Most likely you want this behind Docker.
  • ipv6 => Listen only on IPv6 interfaces.

Note: More information at https://dovecot.org/doc/dovecot-example.conf

"},{"location":"config/environment/#move_spam_to_junk","title":"MOVE_SPAM_TO_JUNK","text":"

When enabled, e-mails marked with the

  1. X-Spam: Yes header added by Rspamd
  2. X-Spam-Flag: YES header added by SpamAssassin (requires SPAMASSASSIN_SPAM_TO_INBOX=1)

will be automatically moved to the Junk folder (with the help of a Sieve script).

  • 0 => Spam messages will be delivered in the mailbox.
  • 1 => Spam messages will be delivered in the Junk folder.
"},{"location":"config/environment/#mark_spam_as_read","title":"MARK_SPAM_AS_READ","text":"

Enable to treat received spam as \"read\" (avoids notification to MUA client of new mail).

Mail is received as spam when it has been marked with either header:

  1. X-Spam: Yes (by Rspamd)
  2. X-Spam-Flag: YES (by SpamAssassin - requires SPAMASSASSIN_SPAM_TO_INBOX=1)

  3. 0 => disabled

  4. 1 => Spam messages will be marked as read
"},{"location":"config/environment/#rspamd","title":"Rspamd","text":""},{"location":"config/environment/#enable_rspamd","title":"ENABLE_RSPAMD","text":"

Enable or disable Rspamd.

  • 0 => disabled
  • 1 => enabled
"},{"location":"config/environment/#enable_rspamd_redis","title":"ENABLE_RSPAMD_REDIS","text":"

Explicit control over running a Redis instance within the container. By default, this value will match what is set for ENABLE_RSPAMD.

The purpose of this setting is to opt-out of starting an internal Redis instance when enabling Rspamd, replacing it with your own external instance.

Configuring Rspamd for an external Redis instance

You will need to provide configuration at /etc/rspamd/local.d/redis.conf similar to:

servers = \"redis.example.test:6379\";\nexpand_keys = true;\n
  • 0 => Disabled
  • 1 => Enabled
"},{"location":"config/environment/#rspamd_check_authenticated","title":"RSPAMD_CHECK_AUTHENTICATED","text":"

This settings controls whether checks should be performed on emails coming from authenticated users (i.e. most likely outgoing emails). The default value is 0 in order to align better with SpamAssassin. We recommend reading through the Rspamd documentation on scanning outbound emails though to decide for yourself whether you need and want this feature.

Not all checks and actions are disabled

DKIM signing of e-mails will still happen.

  • 0 => No checks will be performed for authenticated users
  • 1 => All default checks will be performed for authenticated users
"},{"location":"config/environment/#rspamd_greylisting","title":"RSPAMD_GREYLISTING","text":"

Controls whether the Rspamd Greylisting module is enabled. This module can further assist in avoiding spam emails by greylisting e-mails with a certain spam score.

  • 0 => Disabled
  • 1 => Enabled
"},{"location":"config/environment/#rspamd_learn","title":"RSPAMD_LEARN","text":"

When enabled,

  1. the \"autolearning\" feature is turned on;
  2. the Bayes classifier will be trained (with the help of Sieve scripts) when moving mails
    1. from anywhere to the Junk folder (learning this email as spam);
    2. from the Junk folder into the INBOX (learning this email as ham).

Attention

As of now, the spam learning database is global (i.e. available to all users). If one user deliberately trains it with malicious data, then it will ruin your detection rate.

This feature is suitably only for users who can tell ham from spam and users that can be trusted.

  • 0 => Disabled
  • 1 => Enabled
"},{"location":"config/environment/#rspamd_hfilter","title":"RSPAMD_HFILTER","text":"

Can be used to enable or disable the Hfilter group module. This is used by DMS to adjust the HFILTER_HOSTNAME_UNKNOWN symbol, increasing its default weight to act similar to Postfix's reject_unknown_client_hostname, without the need to outright reject a message.

  • 0 => Disabled
  • 1 => Enabled
"},{"location":"config/environment/#rspamd_hfilter_hostname_unknown_score","title":"RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE","text":"

Can be used to control the score when the HFILTER_HOSTNAME_UNKNOWN symbol applies. A higher score is more punishing. Setting it to 15 (the default score for rejecting an e-mail) is equivalent to rejecting the email when the check fails.

Default: 6 (which corresponds to the add_header action)

"},{"location":"config/environment/#reports","title":"Reports","text":""},{"location":"config/environment/#pflogsumm_trigger","title":"PFLOGSUMM_TRIGGER","text":"

Enables regular Postfix log summary (\"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.

"},{"location":"config/environment/#pflogsumm_recipient","title":"PFLOGSUMM_RECIPIENT","text":"

Recipient address for Postfix log summary reports.

  • not set => Use POSTMASTER_ADDRESS
  • => Specify the recipient address(es)
"},{"location":"config/environment/#pflogsumm_sender","title":"PFLOGSUMM_SENDER","text":"

Sender address (FROM) for pflogsumm reports (if Postfix log summary reports are enabled).

  • not set => Use REPORT_SENDER
  • => Specify the sender address
"},{"location":"config/environment/#logwatch_interval","title":"LOGWATCH_INTERVAL","text":"

Interval for logwatch report.

  • none => No report is generated
  • daily => Send a daily report
  • weekly => Send a report every week
"},{"location":"config/environment/#logwatch_recipient","title":"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)
"},{"location":"config/environment/#logwatch_sender","title":"LOGWATCH_SENDER","text":"

Sender address (FROM) for logwatch reports if logwatch reports are enabled.

  • not set => Use REPORT_SENDER
  • => Specify the sender address
"},{"location":"config/environment/#report_recipient","title":"REPORT_RECIPIENT","text":"

Defines who receives reports (if they are enabled).

  • empty => Use POSTMASTER_ADDRESS
  • => Specify the recipient address
"},{"location":"config/environment/#report_sender","title":"REPORT_SENDER","text":"

Defines who sends reports (if they are enabled).

  • empty => mailserver-report@<YOUR DOMAIN>
  • => Specify the sender address
"},{"location":"config/environment/#logrotate_interval","title":"LOGROTATE_INTERVAL","text":"

Changes the interval in which log files are rotated.

  • weekly => Rotate log files weekly
  • daily => Rotate log files daily
  • monthly => Rotate log files monthly

Note

LOGROTATE_INTERVAL only manages logrotate within the container for services we manage internally.

The entire log output for the container is still available via docker logs mailserver (or your respective container name). If you want to configure external log rotation for that container output as well, : Docker Logging Drivers.

By default, the logs are lost when the container is destroyed (eg: re-creating via docker compose down && docker compose up -d). To keep the logs, mount a volume (to /var/log/mail/).

Note

This variable can also determine the interval for Postfix's log summary reports, see PFLOGSUMM_TRIGGER.

"},{"location":"config/environment/#spamassassin","title":"SpamAssassin","text":""},{"location":"config/environment/#enable_spamassassin","title":"ENABLE_SPAMASSASSIN","text":"
  • 0 => SpamAssassin is disabled
  • 1 => SpamAssassin is enabled
"},{"location":"config/environment/#spamassassin_spam_to_inbox","title":"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.
"},{"location":"config/environment/#enable_spamassassin_kam","title":"ENABLE_SPAMASSASSIN_KAM","text":"

KAM is a 3rd party SpamAssassin ruleset, provided by the McGrail Foundation. If SpamAssassin is enabled, KAM can be used in addition to the default ruleset.

  • 0 => KAM disabled
  • 1 => KAM enabled
"},{"location":"config/environment/#sa_tag","title":"SA_TAG","text":"
  • 2.0 => add spam info headers if at, or above that level

Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1

"},{"location":"config/environment/#sa_tag2","title":"SA_TAG2","text":"
  • 6.31 => add 'spam detected' headers at that level

Note: this SpamAssassin setting needs ENABLE_SPAMASSASSIN=1

"},{"location":"config/environment/#sa_kill","title":"SA_KILL","text":"
  • 10.0 => triggers spam evasive actions

This SpamAssassin setting needs ENABLE_SPAMASSASSIN=1

By default, DMS 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. To inhibit this behaviour and deliver spam emails, set this to a very high value e.g. 100.0.

If ONE_DIR=1 (default) the location is /var/mail-state/lib-amavis/virusmails/, or if ONE_DIR=0: /var/lib/amavis/virusmails/. These paths are inside the docker container.

"},{"location":"config/environment/#sa_spam_subject","title":"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_)***.

"},{"location":"config/environment/#sa_shortcircuit_bayes_spam","title":"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.

"},{"location":"config/environment/#sa_shortcircuit_bayes_ham","title":"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.

"},{"location":"config/environment/#fetchmail","title":"Fetchmail","text":""},{"location":"config/environment/#enable_fetchmail","title":"ENABLE_FETCHMAIL","text":"
  • 0 => fetchmail disabled
  • 1 => fetchmail enabled
"},{"location":"config/environment/#fetchmail_poll","title":"FETCHMAIL_POLL","text":"
  • 300 => fetchmail The number of seconds for the interval
"},{"location":"config/environment/#fetchmail_parallel","title":"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 separate fetchmail instance is started to allow having multiple imap idle connections per server (when poll entries reference the same IMAP server).

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.

"},{"location":"config/environment/#getmail","title":"Getmail","text":""},{"location":"config/environment/#enable_getmail","title":"ENABLE_GETMAIL","text":"

Enable or disable getmail.

  • 0 => Disabled
  • 1 => Enabled
"},{"location":"config/environment/#getmail_poll","title":"GETMAIL_POLL","text":"
  • 5 => getmail The number of minutes for the interval. Min: 1; Max: 30; Default: 5.
"},{"location":"config/environment/#ldap","title":"LDAP","text":""},{"location":"config/environment/#ldap_start_tls","title":"LDAP_START_TLS","text":"
  • empty => no
  • yes => LDAP over TLS enabled for Postfix
"},{"location":"config/environment/#ldap_server_host","title":"LDAP_SERVER_HOST","text":"
  • empty => mail.example.com
  • => Specify the <dns-name> / <ip-address> where the LDAP server is reachable via a URI like: ldaps://mail.example.com.
  • Note: You must include the desired URI scheme (ldap://, ldaps://, ldapi://).
"},{"location":"config/environment/#ldap_search_base","title":"LDAP_SEARCH_BASE","text":"
  • empty => ou=people,dc=domain,dc=com
  • => e.g. LDAP_SEARCH_BASE=dc=mydomain,dc=local
"},{"location":"config/environment/#ldap_bind_dn","title":"LDAP_BIND_DN","text":"
  • empty => cn=admin,dc=domain,dc=com
  • => take a look at examples of SASL_LDAP_BIND_DN
"},{"location":"config/environment/#ldap_bind_pw","title":"LDAP_BIND_PW","text":"
  • empty => admin
  • => Specify the password to bind against ldap
"},{"location":"config/environment/#ldap_query_filter_user","title":"LDAP_QUERY_FILTER_USER","text":"
  • e.g. (&(mail=%s)(mailEnabled=TRUE))
  • => Specify how ldap should be asked for users
"},{"location":"config/environment/#ldap_query_filter_group","title":"LDAP_QUERY_FILTER_GROUP","text":"
  • e.g. (&(mailGroupMember=%s)(mailEnabled=TRUE))
  • => Specify how ldap should be asked for groups
"},{"location":"config/environment/#ldap_query_filter_alias","title":"LDAP_QUERY_FILTER_ALIAS","text":"
  • e.g. (&(mailAlias=%s)(mailEnabled=TRUE))
  • => Specify how ldap should be asked for aliases
"},{"location":"config/environment/#ldap_query_filter_domain","title":"LDAP_QUERY_FILTER_DOMAIN","text":"
  • e.g. (&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))
  • => Specify how ldap should be asked for domains
"},{"location":"config/environment/#ldap_query_filter_senders","title":"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
"},{"location":"config/environment/#dovecot_tls","title":"DOVECOT_TLS","text":"
  • empty => no
  • yes => LDAP over TLS enabled for Dovecot
"},{"location":"config/environment/#dovecot","title":"Dovecot","text":"

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

"},{"location":"config/environment/#dovecot_base","title":"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)
"},{"location":"config/environment/#dovecot_default_pass_scheme","title":"DOVECOT_DEFAULT_PASS_SCHEME","text":"
  • empty => SSHA
  • => Select one crypt scheme for password hashing from this list of password schemes.
"},{"location":"config/environment/#dovecot_dn","title":"DOVECOT_DN","text":"
  • empty => same as LDAP_BIND_DN
  • => Bind dn for LDAP connection. (e.g. cn=admin,dc=domain,dc=com)
"},{"location":"config/environment/#dovecot_dnpass","title":"DOVECOT_DNPASS","text":"
  • empty => same as LDAP_BIND_PW
  • => Password for LDAP dn specified in DOVECOT_DN.
"},{"location":"config/environment/#dovecot_uris","title":"DOVECOT_URIS","text":"
  • empty => same as LDAP_SERVER_HOST
  • => Specify a space separated list of LDAP URIs.
  • Note: You must include the desired URI scheme (ldap://, ldaps://, ldapi://).
"},{"location":"config/environment/#dovecot_ldap_version","title":"DOVECOT_LDAP_VERSION","text":"
  • empty => 3
  • 2 => LDAP version 2 is used
  • 3 => LDAP version 3 is used
"},{"location":"config/environment/#dovecot_auth_bind","title":"DOVECOT_AUTH_BIND","text":"
  • empty => no
  • yes => Enable LDAP authentication binds
"},{"location":"config/environment/#dovecot_user_filter","title":"DOVECOT_USER_FILTER","text":"
  • e.g. (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
"},{"location":"config/environment/#dovecot_user_attrs","title":"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
"},{"location":"config/environment/#dovecot_pass_filter","title":"DOVECOT_PASS_FILTER","text":"
  • e.g. (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))
  • empty => same as DOVECOT_USER_FILTER
"},{"location":"config/environment/#dovecot_pass_attrs","title":"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
"},{"location":"config/environment/#postgrey","title":"Postgrey","text":""},{"location":"config/environment/#enable_postgrey","title":"ENABLE_POSTGREY","text":"
  • 0 => postgrey is disabled
  • 1 => postgrey is enabled
"},{"location":"config/environment/#postgrey_delay","title":"POSTGREY_DELAY","text":"
  • 300 => greylist for N seconds

Note: This postgrey setting needs ENABLE_POSTGREY=1

"},{"location":"config/environment/#postgrey_max_age","title":"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

"},{"location":"config/environment/#postgrey_auto_whitelist_clients","title":"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

"},{"location":"config/environment/#postgrey_text","title":"POSTGREY_TEXT","text":"
  • Delayed by Postgrey => response when a mail is greylisted

Note: This postgrey setting needs ENABLE_POSTGREY=1

"},{"location":"config/environment/#sasl-auth","title":"SASL Auth","text":""},{"location":"config/environment/#enable_saslauthd","title":"ENABLE_SASLAUTHD","text":"
  • 0 => saslauthd is disabled
  • 1 => saslauthd is enabled
"},{"location":"config/environment/#saslauthd_mechanisms","title":"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
"},{"location":"config/environment/#saslauthd_mech_options","title":"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
"},{"location":"config/environment/#saslauthd_ldap_server","title":"SASLAUTHD_LDAP_SERVER","text":"
  • empty => same as LDAP_SERVER_HOST
  • Note: You must include the desired URI scheme (ldap://, ldaps://, ldapi://).
"},{"location":"config/environment/#saslauthd_ldap_start_tls","title":"SASLAUTHD_LDAP_START_TLS","text":"
  • empty => no
  • yes => Enable ldap_start_tls option
"},{"location":"config/environment/#saslauthd_ldap_tls_check_peer","title":"SASLAUTHD_LDAP_TLS_CHECK_PEER","text":"
  • empty => no
  • yes => Enable ldap_tls_check_peer option
"},{"location":"config/environment/#saslauthd_ldap_tls_cacert_dir","title":"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
"},{"location":"config/environment/#saslauthd_ldap_tls_cacert_file","title":"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
"},{"location":"config/environment/#saslauthd_ldap_bind_dn","title":"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
"},{"location":"config/environment/#saslauthd_ldap_password","title":"SASLAUTHD_LDAP_PASSWORD","text":"
  • empty => same as LDAP_BIND_PW
"},{"location":"config/environment/#saslauthd_ldap_search_base","title":"SASLAUTHD_LDAP_SEARCH_BASE","text":"
  • empty => same as LDAP_SEARCH_BASE
  • specify the search base
"},{"location":"config/environment/#saslauthd_ldap_filter","title":"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))
"},{"location":"config/environment/#saslauthd_ldap_password_attr","title":"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
"},{"location":"config/environment/#saslauthd_ldap_auth_method","title":"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
"},{"location":"config/environment/#saslauthd_ldap_mech","title":"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
"},{"location":"config/environment/#srs-sender-rewriting-scheme","title":"SRS (Sender Rewriting Scheme)","text":""},{"location":"config/environment/#srs_sender_classes","title":"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
"},{"location":"config/environment/#srs_exclude_domains","title":"SRS_EXCLUDE_DOMAINS","text":"
  • empty => Envelope sender will be rewritten for all domains
  • provide comma separated list of domains to exclude from rewriting
"},{"location":"config/environment/#srs_secret","title":"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
"},{"location":"config/environment/#srs_domainname","title":"SRS_DOMAINNAME","text":"
  • empty => Derived from OVERRIDE_HOSTNAME, $DOMAINNAME (internal), 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
"},{"location":"config/environment/#default-relay-host","title":"Default Relay Host","text":""},{"location":"config/environment/#default_relay_host","title":"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).
"},{"location":"config/environment/#multi-domain-relay-hosts","title":"Multi-domain Relay Hosts","text":""},{"location":"config/environment/#relay_host","title":"RELAY_HOST","text":"
  • empty => don't configure relay host
  • default host to relay mail through
"},{"location":"config/environment/#relay_port","title":"RELAY_PORT","text":"
  • empty => 25
  • default port to relay mail through
"},{"location":"config/environment/#relay_user","title":"RELAY_USER","text":"
  • empty => no default
  • default relay username (if no specific entry exists in postfix-sasl-password.cf)
"},{"location":"config/environment/#relay_password","title":"RELAY_PASSWORD","text":"
  • empty => no default
  • password for default relay user
"},{"location":"config/pop3/","title":"Mail Delivery with 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 compose.yaml:

mailserver:\nports:\n- \"25:25\"    # SMTP  (explicit TLS => STARTTLS)\n- \"143:143\"  # IMAP4 (explicit TLS => STARTTLS)\n- \"465:465\"  # ESMTP (implicit TLS)\n- \"587:587\"  # ESMTP (explicit TLS => STARTTLS)\n- \"993:993\"  # IMAP4 (implicit TLS)\n- \"110:110\"  # POP3\n- \"995:995\"  # POP3 (with TLS)\nenvironment:\n- ENABLE_POP3=1\n
"},{"location":"config/setup.sh/","title":"About setup.sh","text":"

Note

setup.sh is not required. We encourage you to use docker exec -ti <CONTAINER NAME> setup instead.

Warning

This script assumes Docker or Podman is used. You will not be able to use setup.sh with other container orchestration tools.

setup.sh is a script that is complimentary to the internal setup command in DMS.

It mostly provides the convenience of aliasing docker exec -ti <CONTAINER NAME> setup, inferring the container name of a running DMS instance or running a new instance and bind mounting necessary volumes implicitly.

It is intended to be run from the host machine, not from inside your running container. The latest version of the script is included in the DMS 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\nchmod a+x ./setup.sh\n

For more information on using the script run: ./setup.sh help.

"},{"location":"config/user-management/","title":"User Management","text":""},{"location":"config/user-management/#accounts","title":"Accounts","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 command inside the container. Just run docker exec <CONTAINER NAME> setup help and have a look at the section about subcommands, specifically the email subcommand.

"},{"location":"config/user-management/#adding-a-new-account","title":"Adding a new Account","text":""},{"location":"config/user-management/#via-setup-inside-the-container","title":"Via setup inside the container","text":"

You can add an account by running docker exec -ti <CONTAINER NAME> setup email add <NEW ADDRESS>. This method is strongly preferred.

"},{"location":"config/user-management/#manually","title":"Manually","text":"

Warning

This method is discouraged!

Alternatively, you may directly add the full email address and its encrypted password, separated by a pipe. To generate a new mail account data, directly from your host, you could for example run the following:

docker run --rm -it                                      \\\n--env MAIL_USER=user1@example.com                      \\\n--env MAIL_PASS=mypassword                             \\\nghcr.io/docker-mailserver/docker-mailserver:latest     \\\n/bin/bash -c \\\n'echo \"${MAIL_USER}|$(doveadm pw -s SHA512-CRYPT -u ${MAIL_USER} -p ${MAIL_PASS})\" >>docker-data/dms/config/postfix-accounts.cf'\n

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 docker-data/dms/config/postfix-accounts.cf file of your running container.

The result could look like this:

user1@example.com|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1\n
"},{"location":"config/user-management/#quotas","title":"Quotas","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 (PRs are welcome!).
"},{"location":"config/user-management/#aliases","title":"Aliases","text":"

The best way to manage aliases is to use the reliable setup script inside the container. Just run docker exec <CONTAINER NAME> setup help and have a look at the section about subcommands, specifically the alias-subcommand.

"},{"location":"config/user-management/#about","title":"About","text":"

You may read Postfix's documentation on virtual aliases first. 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 example.com as its domain:

# Alias delivered to an existing account\nalias1@example.com user1@example.com\n\n# Alias forwarded to an external email address\nalias2@example.com external-account@gmail.com\n
"},{"location":"config/user-management/#configuring-regexp-aliases","title":"Configuring RegExp Aliases","text":"

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

/^test[0-9][0-9]*@example.com/ qa@example.com\n
"},{"location":"config/user-management/#address-tags-extension-delimiters-as-an-alternative-to-aliases","title":"Address Tags (Extension Delimiters) as 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 Postfix's official documentation.

Note

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

recipient_delimiter = -\n
"},{"location":"config/advanced/auth-ldap/","title":"Advanced | LDAP Authentication","text":""},{"location":"config/advanced/auth-ldap/#introduction","title":"Introduction","text":"

Getting started with ldap and DMS 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)
"},{"location":"config/advanced/auth-ldap/#variables-to-control-provisioning-by-the-container","title":"Variables to Control Provisioning by the Container","text":"

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

"},{"location":"config/advanced/auth-ldap/#ldap_query_filter_","title":"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.

- LDAP_START_TLS=yes\n- ACCOUNT_PROVISIONER=LDAP\n- LDAP_SERVER_HOST=ldap.example.org\n- LDAP_SEARCH_BASE=dc=example,dc=org\"\n- LDAP_BIND_DN=cn=admin,dc=example,dc=org\n- LDAP_BIND_PW=mypassword\n- SPOOF_PROTECTION=1\n\n- LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s)\n- LDAP_QUERY_FILTER_USER=(mail=%s)\n- LDAP_QUERY_FILTER_ALIAS=(|) # doesn't match anything\n- LDAP_QUERY_FILTER_GROUP=(|) # doesn't match anything\n- LDAP_QUERY_FILTER_SENDERS=(|(mail=%s)(mail=admin@*))\n
"},{"location":"config/advanced/auth-ldap/#dovecot__filter-dovecot__attrs","title":"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=<YOUR_USER_IDENTIFIER_ATTRIBUTE>=user,<YOUR_USER_PASSWORD_ATTRIBUTE>=password\n- 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\n

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\n- DOVECOT_PASS_ATTRS=uniqueIdentifier=user,userPassword=password\n- DOVECOT_USER_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))\n
Example

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

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

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

"},{"location":"config/advanced/auth-ldap/#dovecot_auth_bind","title":"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.

"},{"location":"config/advanced/auth-ldap/#saslauthd_ldap_filter","title":"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 separately 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\n- SASLAUTHD_MECHANISMS=ldap\n- SASLAUTHD_LDAP_FILTER=(mail=%U@example.org)\n
"},{"location":"config/advanced/auth-ldap/#secure-connection-with-ldaps-or-starttls","title":"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\n- DOVECOT_TLS=yes\n- SASLAUTHD_LDAP_START_TLS=yes\n
"},{"location":"config/advanced/auth-ldap/#active-directory-configurations-tested-with-samba4-ad-implementation","title":"Active Directory Configurations (Tested with Samba4 AD Implementation)","text":"

In addition to LDAP explanation above, when Docker Mailserver is intended to be used with Active Directory (or the equivalent implementations like Samba4 AD DC) the following points should be taken into consideration:

  • Samba4 Active Directory requires a secure connection to the domain controller (DC), either via SSL/TLS (LDAPS) or via StartTLS.
  • The username equivalent in Active Directory is: sAMAccountName.
  • proxyAddresses can be used to store email aliases of single users. The convention is to prefix the email aliases with smtp: (e.g: smtp:some.name@example.com).
  • Active Directory is used typically not only as LDAP Directory storage, but also as a domain controller, i.e., it will do many things including authenticating users. Mixing Linux and Windows clients requires the usage of RFC2307 attributes, namely uidNumber, gidNumber instead of the typical uid. Assigning different owner to email folders can also be done in this approach, nevertheless there is a bug at the moment in Docker Mailserver that overwrites all permissions when starting the container. Either a manual fix is necessary now, or a temporary workaround to use a hard-coded ldap:uidNumber that equals to 5000 until this issue is fixed.
  • To deliver the emails to different members of Active Directory Security Group or Distribution Group (similar to mailing lists), use a user-patches.sh script to modify ldap-groups.cf so that it includes leaf_result_attribute = mail and special_result_attribute = member. This can be achieved simply by:

The configuration shown to get the Group to work is from here and here.

# user-patches.sh\n\n...\ngrep -q '^leaf_result_attribute = mail$' /etc/postfix/ldap-groups.cf || echo \"leaf_result_attribute = mail\" >> /etc/postfix/ldap-groups.cf\ngrep -q '^special_result_attribute = member$' /etc/postfix/ldap-groups.cf || echo \"special_result_attribute = member\" >> /etc/postfix/ldap-groups.cf\n...\n
  • In /etc/ldap/ldap.conf, if the TLS_REQCERT is demand / hard (default), the CA certificate used to verify the LDAP server certificate must be recognized as a trusted CA. This can be done by volume mounting the ca.crt file and updating the trust store via a user-patches.sh script:
# user-patches.sh\n\n...\ncp /MOUNTED_FOLDER/ca.crt /usr/local/share/ca-certificates/\nupdate-ca-certificates\n...\n

The changes on the configurations necessary to work with Active Directory (only changes are listed, the rest of the LDAP configuration can be taken from the other examples shown in this documentation):

# If StartTLS is the chosen method to establish a secure connection with Active Directory.\n- LDAP_START_TLS=yes\n- SASLAUTHD_LDAP_START_TLS=yes\n- DOVECOT_TLS=yes\n\n- LDAP_QUERY_FILTER_USER=(&(objectclass=person)(mail=%s))\n- LDAP_QUERY_FILTER_ALIAS=(&(objectclass=person)(proxyAddresses=smtp:%s))\n# Filters Active Directory groups (mail lists). Additional changes on ldap-groups.cf are also required as shown above.\n- LDAP_QUERY_FILTER_GROUP=(&(objectClass=group)(mail=%s))\n- LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s)\n# Allows only Domain admins to send any sender email address, otherwise the sender address must match the LDAP attribute `mail`.\n- SPOOF_PROTECTION=1\n- LDAP_QUERY_FILTER_SENDERS=(|(mail=%s)(proxyAddresses=smtp:%s)(memberOf=cn=Domain Admins,cn=Users,dc=*))\n\n- DOVECOT_USER_FILTER=(&(objectclass=person)(sAMAccountName=%n))\n# At the moment to be able to use %{ldap:uidNumber}, a manual bug fix as described above must be used. Otherwise %{ldap:uidNumber} %{ldap:uidNumber} must be replaced by the hard-coded value 5000.\n- DOVECOT_USER_ATTRS==uid=%{ldap:uidNumber},=gid=5000,=home=/var/mail/%Ln,=mail=maildir:~/Maildir\n- DOVECOT_PASS_ATTRS=sAMAccountName=user,userPassword=password\n- SASLAUTHD_LDAP_FILTER=(&(sAMAccountName=%U)(objectClass=person))\n
"},{"location":"config/advanced/auth-ldap/#ldap-setup-examples","title":"LDAP Setup Examples","text":"Basic Setup
services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\ncontainer_name: mailserver\nhostname: mail.example.com\n\nports:\n- \"25:25\"\n- \"143:143\"\n- \"587:587\"\n- \"993:993\"\n\nvolumes:\n- ./docker-data/dms/mail-data/:/var/mail/\n- ./docker-data/dms/mail-state/:/var/mail-state/\n- ./docker-data/dms/mail-logs/:/var/log/mail/\n- ./docker-data/dms/config/:/tmp/docker-mailserver/\n- /etc/localtime:/etc/localtime:ro\n\nenvironment:\n- ENABLE_SPAMASSASSIN=1\n- ENABLE_CLAMAV=1\n- ENABLE_FAIL2BAN=1\n- ENABLE_POSTGREY=1\n\n# >>> Postfix LDAP Integration\n- ACCOUNT_PROVISIONER=LDAP\n- LDAP_SERVER_HOST=ldap.example.org\n- LDAP_BIND_DN=cn=admin,ou=users,dc=example,dc=org\n- LDAP_BIND_PW=mypassword\n- LDAP_SEARCH_BASE=dc=example,dc=org\n- LDAP_QUERY_FILTER_DOMAIN=(|(mail=*@%s)(mailAlias=*@%s)(mailGroupMember=*@%s))\n- LDAP_QUERY_FILTER_USER=(&(objectClass=inetOrgPerson)(mail=%s))\n- LDAP_QUERY_FILTER_ALIAS=(&(objectClass=inetOrgPerson)(mailAlias=%s))\n- LDAP_QUERY_FILTER_GROUP=(&(objectClass=inetOrgPerson)(mailGroupMember=%s))\n- LDAP_QUERY_FILTER_SENDERS=(&(objectClass=inetOrgPerson)(|(mail=%s)(mailAlias=%s)(mailGroupMember=%s)))\n- SPOOF_PROTECTION=1\n# <<< Postfix LDAP Integration\n\n# >>> Dovecot LDAP Integration\n- DOVECOT_USER_FILTER=(&(objectClass=inetOrgPerson)(mail=%u))\n- DOVECOT_PASS_ATTRS=uid=user,userPassword=password\n- DOVECOT_USER_ATTRS==home=/var/mail/%{ldap:uid},=mail=maildir:~/Maildir,uidNumber=uid,gidNumber=gid\n# <<< Dovecot LDAP Integration\n\n# >>> SASL LDAP Authentication\n- ENABLE_SASLAUTHD=1\n- SASLAUTHD_MECHANISMS=ldap\n- SASLAUTHD_LDAP_FILTER=(&(mail=%U@example.org)(objectClass=inetOrgPerson))\n# <<< SASL LDAP Authentication\n\n- SSL_TYPE=letsencrypt\n- PERMIT_DOCKER=host\n\ncap_add:\n- NET_ADMIN\n
Kopano / Zarafa
services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\ncontainer_name: mailserver\nhostname: mail.example.com\n\nports:\n- \"25:25\"\n- \"143:143\"\n- \"587:587\"\n- \"993:993\"\n\nvolumes:\n- ./docker-data/dms/mail-data/:/var/mail/\n- ./docker-data/dms/mail-state/:/var/mail-state/\n- ./docker-data/dms/config/:/tmp/docker-mailserver/\n\nenvironment:\n# We are not using dovecot here\n- SMTP_ONLY=1\n- ENABLE_SPAMASSASSIN=1\n- ENABLE_CLAMAV=1\n- ENABLE_FAIL2BAN=1\n- ENABLE_POSTGREY=1\n- SASLAUTHD_PASSWD=\n\n# >>> SASL Authentication\n- ENABLE_SASLAUTHD=1\n- SASLAUTHD_LDAP_FILTER=(&(sAMAccountName=%U)(objectClass=person))\n- SASLAUTHD_MECHANISMS=ldap\n# <<< SASL Authentication\n\n# >>> Postfix Ldap Integration\n- ACCOUNT_PROVISIONER=LDAP\n- LDAP_SERVER_HOST=<yourLdapContainer/yourLdapServer>\n- LDAP_SEARCH_BASE=dc=mydomain,dc=loc\n- LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=loc\n- LDAP_BIND_PW=mypassword\n- LDAP_QUERY_FILTER_USER=(&(objectClass=user)(mail=%s))\n- LDAP_QUERY_FILTER_GROUP=(&(objectclass=group)(mail=%s))\n- LDAP_QUERY_FILTER_ALIAS=(&(objectClass=user)(otherMailbox=%s))\n- LDAP_QUERY_FILTER_DOMAIN=(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))\n# <<< Postfix Ldap Integration\n\n# >>> Kopano Integration\n- POSTFIX_DAGENT=lmtp:kopano:2003\n# <<< Kopano Integration\n\n- SSL_TYPE=letsencrypt\n- PERMIT_DOCKER=host\n\ncap_add:\n- NET_ADMIN\n
"},{"location":"config/advanced/dovecot-master-accounts/","title":"Advanced | Dovecot master accounts","text":""},{"location":"config/advanced/dovecot-master-accounts/#introduction","title":"Introduction","text":"

A dovecot master account is able to login as any configured user. This is useful for administrative tasks like hot backups.

"},{"location":"config/advanced/dovecot-master-accounts/#configuration","title":"Configuration","text":"

It is possible to create, update, delete and list dovecot master accounts using setup.sh. See setup.sh help for usage.

This feature is presently not supported with LDAP.

"},{"location":"config/advanced/dovecot-master-accounts/#logging-in","title":"Logging in","text":"

Once a master account is configured, it is possible to connect to any users mailbox using this account. Log in over POP3/IMAP using the following credential scheme:

Username: <EMAIL ADDRESS>*<MASTER ACCOUNT NAME>

Password: <MASTER ACCOUNT PASSWORD>

"},{"location":"config/advanced/full-text-search/","title":"Advanced | Full-Text Search","text":""},{"location":"config/advanced/full-text-search/#overview","title":"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. Dovecot supports a variety of community supported FTS indexing backends.

DMS comes pre-installed with two plugins that can be enabled with a dovecot config file.

Please be aware that indexing consumes memory and takes up additional disk space.

"},{"location":"config/advanced/full-text-search/#xapian","title":"Xapian","text":"

The dovecot-fts-xapian plugin makes use of Xapian. Xapian enables embedding an FTS engine without the need for additional backends.

The indexes will be stored as a subfolder named xapian-indexes inside your local mail-data folder (/var/mail internally). With the default settings, 10GB of email data may generate around 4GB of indexed data.

While indexing is memory intensive, you can configure the plugin to limit the amount of memory consumed by the index workers. With Xapian being small and fast, this plugin is a good choice for low memory environments (2GB) as compared to Solr.

"},{"location":"config/advanced/full-text-search/#setup","title":"Setup","text":"
  1. To configure fts-xapian as a dovecot plugin, create a file at docker-data/dms/config/dovecot/fts-xapian-plugin.conf and place the following in it:

    mail_plugins = $mail_plugins fts fts_xapian\n\nplugin {\n    fts = xapian\n    fts_xapian = partial=3 full=20 verbose=0\n\n    fts_autoindex = yes\n    fts_enforced = yes\n\n    # disable indexing of folders\n    # fts_autoindex_exclude = \\Trash\n\n    # Index attachements\n    # fts_decoder = decode2text\n}\n\nservice indexer-worker {\n    # limit size of indexer-worker RAM usage, ex: 512MB, 1GB, 2GB\n    vsz_limit = 1GB\n}\n\n# service decode2text {\n#     executable = script /usr/libexec/dovecot/decode2text.sh\n#     user = dovecot\n#     unix_listener decode2text {\n#         mode = 0666\n#     }\n# }\n

    adjust the settings to tune for your desired memory limits, exclude folders and enable searching text inside of attachments

  2. Update compose.yaml to load the previously created dovecot plugin config file:

      services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\ncontainer_name: mailserver\nhostname: mail.example.com\nenv_file: mailserver.env\nports:\n- \"25:25\"    # SMTP  (explicit TLS => STARTTLS)\n- \"143:143\"  # IMAP4 (explicit TLS => STARTTLS)\n- \"465:465\"  # ESMTP (implicit TLS)\n- \"587:587\"  # ESMTP (explicit TLS => STARTTLS)\n- \"993:993\"  # IMAP4 (implicit TLS)\nvolumes:\n- ./docker-data/dms/mail-data/:/var/mail/\n- ./docker-data/dms/mail-state/:/var/mail-state/\n- ./docker-data/dms/mail-logs/:/var/log/mail/\n- ./docker-data/dms/config/:/tmp/docker-mailserver/\n- ./docker-data/dms/config/dovecot/fts-xapian-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro\n- /etc/localtime:/etc/localtime:ro\nrestart: always\nstop_grace_period: 1m\ncap_add:\n- NET_ADMIN\n
  3. Recreate containers:

    docker compose down\ndocker compose up -d\n
  4. Initialize indexing on all users for all mail:

    docker compose exec mailserver doveadm index -A -q \\*\n
  5. Run the following command in a daily cron job:

    docker compose exec mailserver doveadm fts optimize -A\n
    Or like the Spamassassin example shows, you can instead use cron from within DMS to avoid potential errors if the mail server is not running:

Example

Create a system cron file:

# in the compose.yaml root directory\nmkdir -p ./docker-data/dms/cron # if you didn't have this folder before\ntouch ./docker-data/dms/cron/fts_xapian\nchown root:root ./docker-data/dms/cron/fts_xapian\nchmod 0644 ./docker-data/dms/cron/fts_xapian\n

Edit the system cron file nano ./docker-data/dms/cron/fts_xapian, and set an appropriate configuration:

# Adding `MAILTO=\"\"` prevents cron emailing notifications of the task outcome each run\nMAILTO=\"\"\n#\n# m h dom mon dow user command\n#\n# Everyday 4:00AM, optimize index files\n0  4 * * * root  doveadm fts optimize -A\n

Then with compose.yaml:

services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\nvolumes:\n- ./docker-data/dms/cron/fts_xapian:/etc/cron.d/fts_xapian\n
"},{"location":"config/advanced/full-text-search/#solr","title":"Solr","text":"

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.

Solr is a mature and fast indexing backend that runs on the JVM. The indexes are relatively compact compared to the size of your total email.

However, Solr also requires a fair bit of RAM. While Solr is highly tuneable, it may require a bit of testing to get it right.

"},{"location":"config/advanced/full-text-search/#setup_1","title":"Setup","text":"
  1. compose.yaml:

      solr:\nimage: lmmdock/dovecot-solr:latest\nvolumes:\n- ./docker-data/dms/config/dovecot/solr-dovecot:/opt/solr/server/solr/dovecot\nrestart: always\n\nmailserver:\ndepends_on:\n- solr\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\n...\nvolumes:\n...\n- ./docker-data/dms/config/dovecot/10-plugin.conf:/etc/dovecot/conf.d/10-plugin.conf:ro\n...\n
  2. ./docker-data/dms/config/dovecot/10-plugin.conf:

    mail_plugins = $mail_plugins fts fts_solr\n\nplugin {\nfts = solr\nfts_autoindex = yes\nfts_solr = url=http://solr:8983/solr/dovecot/\n}\n
  3. Recreate containers: docker compose down ; docker compose up -d

  4. 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

"},{"location":"config/advanced/full-text-search/#further-discussion","title":"Further Discussion","text":"

See #905

"},{"location":"config/advanced/ipv6/","title":"Advanced | IPv6","text":"

Ample Opportunities for Issues

Numerous bug reports have been raised in the past about IPv6. Please make sure your setup around DMS is correct when using IPv6!

"},{"location":"config/advanced/ipv6/#ipv6-networking-problems-with-docker-defaults","title":"IPv6 networking problems with Docker defaults","text":""},{"location":"config/advanced/ipv6/#what-can-go-wrong","title":"What can go wrong?","text":"

If your host system supports IPv6 and an AAAA DNS record exists to direct IPv6 traffic to DMS, you may experience issues when an IPv6 connection is made:

  • The original client IP is replaced with the gateway IP of a docker network.
  • Connections fail or hang.

The impact of losing the real IP of the client connection can negatively affect DMS:

  • Users unable to login (Fail2Ban action triggered by repeated login failures all seen as from the same internal Gateway IP)
  • Mail inbound to DMS is rejected (SPF verification failure, IP mismatch)
  • Delivery failures from sender reputation being reduced (due to bouncing inbound mail from rejected IPv6 clients)
  • Some services may be configured to trust connecting clients within the containers subnet, which includes the Gateway IP. This can risk bypassing or relaxing security measures, such as exposing an open relay.
"},{"location":"config/advanced/ipv6/#why-does-this-happen","title":"Why does this happen?","text":"

When the host network receives a connection to a containers published port, it is routed to the containers internal network managed by Docker (typically a bridge network).

By default, the Docker daemon only assigns IPv4 addresses to containers, thus it will only accept IPv4 connections (unless a docker-proxy process is listening, which the default daemon setting userland-proxy: true enables). With the daemon setting userland-proxy: true (default), IPv6 connections from the host can also be accepted and routed to containers (even when they only have IPv4 addresses assigned). userland-proxy: false will require the container to have atleast an IPv6 address assigned.

This can be problematic for IPv6 host connections when internally the container is no longer aware of the original client IPv6 address, as it has been proxied through the IPv4 or IPv6 gateway address of it's connected network (eg: 172.17.0.1 - Docker allocates networks from a set of default subnets).

This can be fixed by enabling a Docker network to assign IPv6 addresses to containers, along with some additional configuration. Alternatively you could configure the opposite to prevent IPv6 connections being made.

"},{"location":"config/advanced/ipv6/#prevent-ipv6-connections","title":"Prevent IPv6 connections","text":"
  • Avoiding an AAAA DNS record for your DMS FQDN would prevent resolving an IPv6 address to connect to.
  • You can also use userland-proxy: false, which will fail to establish a remote connection to DMS (provided no IPv6 address was assigned).

With UFW or Firewalld

When one of these firewall frontends are active, remote clients should fail to connect instead of being masqueraded as the docker network gateway IP. Keep in mind that this only affects remote clients, it does not affect local IPv6 connections originating within the same host.

"},{"location":"config/advanced/ipv6/#enable-proper-ipv6-support","title":"Enable proper IPv6 support","text":"

You can enable IPv6 support in Docker for container networks, however compatibility concerns may affect your success.

The official Docker documentation on enabling IPv6 has been improving and is a good resource to reference.

Enable ip6tables support so that Docker will manage IPv6 networking rules as well. This will allow for IPv6 NAT to work like the existing IPv4 NAT already does for your containers, avoiding the above issue with external connections having their IP address seen as the container network gateway IP (provided an IPv6 address is also assigned to the container).

Configure the following in /etc/docker/daemon.json

{\n\"ip6tables\": true,\n\"experimental\" : true,\n\"userland-proxy\": true\n}\n
  • experimental: true is currently required for ip6tables: true to work.
  • userland-proxy setting can potentially affect connection behaviour for local connections.

Now restart the daemon if it's running: systemctl restart docker.

Next, configure a network with an IPv6 subnet for your container with any of these examples:

Create an IPv6 ULA subnet About these examples

These examples are focused on a IPv6 ULA subnet which is suitable for most users as described in the next section.

  • You may prefer a subnet size smaller than /64 (eg: /112, which still provides over 65k IPv6 addresses), especially if instead configuring for an IPv6 GUA subnet.
  • The network will also implicitly be assigned an IPv4 subnet (from the Docker daemon config default-address-pools).
User-defined NetworkDefault Bridge (daemon)

The preferred approach is with user-defined networks via compose.yaml (recommended) or CLI with docker network create:

ComposeCLI

Create the network in compose.yaml and attach a service to it:

compose.yaml
services:\nmailserver:\nnetworks:\n- dms-ipv6\n\nnetworks:\ndms-ipv6:\nenable_ipv6: true\nipam:\nconfig:\n- subnet: fd00:cafe:face:feed::/64\n
Override the implicit default network

You can optionally avoid the service assignment by overriding the default user-defined network that Docker Compose generates. Just replace dms-ipv6 with default.

The Docker Compose default bridge is not affected by settings for the default bridge (aka docker0) in /etc/docker/daemon.json.

Using the network outside of this compose.yaml

To reference this network externally (from other compose files or docker run), assign the networks name key in compose.yaml.

Create the network via a CLI command (which can then be used with docker run --network dms-ipv6):

docker network create --ipv6 --subnet fd00:cafe:face:feed::/64 dms-ipv6\n

Optionally reference it from one or more compose.yaml files:

compose.yaml
services:\nmailserver:\nnetworks:\n- dms-ipv6\n\nnetworks:\ndms-ipv6:\nexternal: true\n

This approach is discouraged

The bridge network is considered legacy.

Add these two extra IPv6 settings to your daemon config. They only apply to the default bridge docker network aka docker0 (which containers are attached to by default when using docker run).

/etc/docker/daemon.json
{\n\"ipv6\": true,\n\"fixed-cidr-v6\": \"fd00:cafe:face:feed::/64\",\n}\n

Compose projects can also use this network via network_mode:

compose.yaml
services:\nmailserver:\nnetwork_mode: bridge\n

Do not use 2001:db8:1::/64 for your private subnet

The 2001:db8 address prefix is reserved for documentation. Avoid creating a subnet with this prefix.

Presently this is used in examples for Dockers IPv6 docs as a placeholder, while mixed in with private IPv4 addresses which can be misleading.

"},{"location":"config/advanced/ipv6/#configuring-an-ipv6-subnet","title":"Configuring an IPv6 subnet","text":"

If you've configured IPv6 address pools in /etc/docker/daemon.json, you do not need to specify a subnet explicitly. Otherwise if you're unsure what value to provide, here's a quick guide (Tip: Prefer IPv6 ULA, it's the least hassle):

  • fd00:cafe:face:feed::/64 is an example of a IPv6 ULA subnet. ULA addresses are akin to the private IPv4 subnets you may already be familiar with. You can use that example, or choose your own ULA address. This is a good choice for getting Docker containers to their have networks support IPv6 via NAT like they already do by default with IPv4.
  • IPv6 without NAT, using public address space like your server is assigned belongs to an IPv6 GUA subnet.
    • Typically these will be a /64 block assigned to your host, but this varies by provider.
    • These addresses do not need to publish ports of a container to another IP to be publicly reached (thus ip6tables: true is not required), you will want a firewall configured to manage which ports are accessible instead as no NAT is involved. Note that this may not be desired if the container should also be reachable via the host IPv4 public address.
    • You may want to subdivide the /64 into smaller subnets for Docker to use only portions of the /64. This can reduce some routing features, and require additional setup / management via a NDP Proxy for your public interface to know of IPv6 assignments managed by Docker and accept external traffic.
"},{"location":"config/advanced/ipv6/#verify-remote-ip-is-correct","title":"Verify remote IP is correct","text":"

With Docker CLI or Docker Compose, run a traefik/whoami container with your IPv6 docker network and port 80 published. You can then send a curl request (or via address in the browser) from another host (as your remote client) with an IPv6 network, the RemoteAddr value returned should match your client IPv6 address.

docker run --rm -d --network dms-ipv6 -p 80:80 traefik/whoami\n# On a different host, replace `2001:db8::1` with your DMS host IPv6 address\ncurl --max-time 5 http://[2001:db8::1]:80\n

IPv6 ULA address priority

DNS lookups that have records for both IPv4 and IPv6 addresses (eg: localhost) may prefer IPv4 over IPv6 (ULA) for private addresses, whereas for public addresses IPv6 has priority. This shouldn't be anything to worry about, but can come across as a surprise when testing your IPv6 setup on the same host instead of from a remote client.

The preference can be controlled with /etc/gai.conf, and appears was configured this way based on the assumption that IPv6 ULA would never be used with NAT. It should only affect the destination resolved for outgoing connections, which for IPv6 ULA should only really affect connections between your containers / host. In future IPv6 ULA may also be prioritized.

"},{"location":"config/advanced/kubernetes/","title":"Advanced | Kubernetes","text":""},{"location":"config/advanced/kubernetes/#introduction","title":"Introduction","text":"

This article describes how to deploy DMS to Kubernetes. Please note that there is also a Helm chart available.

Requirements

We assume basic knowledge about Kubernetes from the reader. Moreover, we assume the reader to have a basic understanding of mail servers. Ideally, the reader has deployed DMS before in an easier setup with Docker (Compose).

About Support for Kubernetes

Please note that Kubernetes is not officially supported and we do not build images specifically designed for it. When opening an issue, please remember that only Docker & Docker Compose are officially supported.

This content is entirely community-supported. If you find errors, please open an issue and provide a PR.

"},{"location":"config/advanced/kubernetes/#manifests","title":"Manifests","text":""},{"location":"config/advanced/kubernetes/#configuration","title":"Configuration","text":"

We want to provide the basic configuration in the form of environment variables with a ConfigMap. Note that this is just an example configuration; tune the ConfigMap to your needs.

---\napiVersion: v1\nkind: ConfigMap\n\nmetadata:\nname: mailserver.environment\n\nimmutable: false\n\ndata:\nTLS_LEVEL: modern\nPOSTSCREEN_ACTION: drop\nOVERRIDE_HOSTNAME: mail.example.com\nFAIL2BAN_BLOCKTYPE: drop\nPOSTMASTER_ADDRESS: postmaster@example.com\nUPDATE_CHECK_INTERVAL: 10d\nPOSTFIX_INET_PROTOCOLS: ipv4\nONE_DIR: '1'\nENABLE_CLAMAV: '1'\nENABLE_POSTGREY: '0'\nENABLE_FAIL2BAN: '1'\nAMAVIS_LOGLEVEL: '-1'\nSPOOF_PROTECTION: '1'\nMOVE_SPAM_TO_JUNK: '1'\nENABLE_UPDATE_CHECK: '1'\nENABLE_SPAMASSASSIN: '1'\nSUPERVISOR_LOGLEVEL: warn\nSPAMASSASSIN_SPAM_TO_INBOX: '1'\n\n# here, we provide an example for the SSL configuration\nSSL_TYPE: manual\nSSL_CERT_PATH: /secrets/ssl/rsa/tls.crt\nSSL_KEY_PATH: /secrets/ssl/rsa/tls.key\n

We can also make use of user-provided configuration files, e.g. user-patches.sh, postfix-accounts.cf and more, to adjust DMS to our likings. We encourage you to have a look at Kustomize for creating ConfigMaps from multiple files, but for now, we will provide a simple, hand-written example. This example is absolutely minimal and only goes to show what can be done.

---\napiVersion: v1\nkind: ConfigMap\n\nmetadata:\nname: mailserver.files\n\ndata:\npostfix-accounts.cf: |\ntest@example.com|{SHA512-CRYPT}$6$someHashValueHere\nother@example.com|{SHA512-CRYPT}$6$someOtherHashValueHere\n

Static Configuration

With the configuration shown above, you can not dynamically add accounts as the configuration file mounted into the mail server can not be written to.

Use persistent volumes for production deployments.

"},{"location":"config/advanced/kubernetes/#persistence","title":"Persistence","text":"

Thereafter, we need persistence for our data. Make sure you have a storage provisioner and that you choose the correct storageClassName.

---\napiVersion: v1\nkind: PersistentVolumeClaim\n\nmetadata:\nname: data\n\nspec:\nstorageClassName: local-path\naccessModes:\n- ReadWriteOnce\nresources:\nrequests:\nstorage: 25Gi\n
"},{"location":"config/advanced/kubernetes/#service","title":"Service","text":"

A Service is required for getting the traffic to the pod itself. The service is somewhat crucial. Its configuration determines whether the original IP from the sender will be kept. More about this further down below.

The configuration you're seeing does keep the original IP, but you will not be able to scale this way. We have chosen to go this route in this case because we think most Kubernetes users will only want to have one instance.

---\napiVersion: v1\nkind: Service\n\nmetadata:\nname: mailserver\nlabels:\napp: mailserver\n\nspec:\ntype: LoadBalancer\n\nselector:\napp: mailserver\n\nports:\n# Transfer\n- name: transfer\nport: 25\ntargetPort: transfer\nprotocol: TCP\n# ESMTP with implicit TLS\n- name: esmtp-implicit\nport: 465\ntargetPort: esmtp-implicit\nprotocol: TCP\n# ESMTP with explicit TLS (STARTTLS)\n- name: esmtp-explicit\nport: 587\ntargetPort: esmtp-explicit\nprotocol: TCP\n# IMAPS with implicit TLS\n- name: imap-implicit\nport: 993\ntargetPort: imap-implicit\nprotocol: TCP\n
"},{"location":"config/advanced/kubernetes/#deployments","title":"Deployments","text":"

Last but not least, the Deployment becomes the most complex component. It instructs Kubernetes how to run the DMS container and how to apply your ConfigMaps, persisted storage, etc. Additionally, we can set options to enforce runtime security here.

---\napiVersion: apps/v1\nkind: Deployment\n\nmetadata:\nname: mailserver\n\nannotations:\nignore-check.kube-linter.io/run-as-non-root: >-\n'mailserver' needs to run as root\nignore-check.kube-linter.io/privileged-ports: >-\n'mailserver' needs privileged ports\nignore-check.kube-linter.io/no-read-only-root-fs: >-\nThere are too many files written to make The\nroot FS read-only\n\nspec:\nreplicas: 1\nselector:\nmatchLabels:\napp: mailserver\n\ntemplate:\nmetadata:\nlabels:\napp: mailserver\n\nannotations:\ncontainer.apparmor.security.beta.kubernetes.io/mailserver: runtime/default\n\nspec:\nhostname: mail\ncontainers:\n- name: mailserver\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\nimagePullPolicy: IfNotPresent\n\nsecurityContext:\n# Required to support SGID via `postdrop` executable\n# in `/var/mail-state` for Postfix (maildrop + public dirs):\n# https://github.com/docker-mailserver/docker-mailserver/pull/3625\nallowPrivilegeEscalation: true\nreadOnlyRootFilesystem: false\nrunAsUser: 0\nrunAsGroup: 0\nrunAsNonRoot: false\nprivileged: false\ncapabilities:\nadd:\n# file permission capabilities\n- CHOWN\n- FOWNER\n- MKNOD\n- SETGID\n- SETUID\n- DAC_OVERRIDE\n# network capabilities\n- NET_ADMIN  # needed for F2B\n- NET_RAW    # needed for F2B\n- NET_BIND_SERVICE\n# miscellaneous  capabilities\n- SYS_CHROOT\n- KILL\ndrop: [ALL]\nseccompProfile:\ntype: RuntimeDefault\n\n# You want to tune this to your needs. If you disable ClamAV,\n#   you can use less RAM and CPU. This becomes important in\n#   case you're low on resources and Kubernetes refuses to\n#   schedule new pods.\nresources:\nlimits:\nmemory: 4Gi\ncpu: 1500m\nrequests:\nmemory: 2Gi\ncpu: 600m\n\nvolumeMounts:\n- name: files\nsubPath: postfix-accounts.cf\nmountPath: /tmp/docker-mailserver/postfix-accounts.cf\nreadOnly: true\n\n# PVCs\n- name: data\nmountPath: /var/mail\nsubPath: data\nreadOnly: false\n- name: data\nmountPath: /var/mail-state\nsubPath: state\nreadOnly: false\n- name: data\nmountPath: /var/log/mail\nsubPath: log\nreadOnly: false\n\n# certificates\n- name: certificates-rsa\nmountPath: /secrets/ssl/rsa/\nreadOnly: true\n\n# other\n- name: tmp-files\nmountPath: /tmp\nreadOnly: false\n\nports:\n- name: transfer\ncontainerPort: 25\nprotocol: TCP\n- name: esmtp-implicit\ncontainerPort: 465\nprotocol: TCP\n- name: esmtp-explicit\ncontainerPort: 587\n- name: imap-implicit\ncontainerPort: 993\nprotocol: TCP\n\nenvFrom:\n- configMapRef:\nname: mailserver.environment\n\nrestartPolicy: Always\n\nvolumes:\n# configuration files\n- name: files\nconfigMap:\nname: mailserver.files\n\n# PVCs\n- name: data\npersistentVolumeClaim:\nclaimName: data\n\n# certificates\n- name: certificates-rsa\nsecret:\nsecretName: mail-tls-certificate-rsa\nitems:\n- key: tls.key\npath: tls.key\n- key: tls.crt\npath: tls.crt\n\n# other\n- name: tmp-files\nemptyDir: {}\n
"},{"location":"config/advanced/kubernetes/#certificates-an-example","title":"Certificates - An Example","text":"

In this example, we use cert-manager to supply RSA certificates. You can also supply RSA certificates as fallback certificates, which DMS supports out of the box with SSL_ALT_CERT_PATH and SSL_ALT_KEY_PATH, and provide ECDSA as the proper certificates.

---\napiVersion: cert-manager.io/v1\nkind: Certificate\n\nmetadata:\nname: mail-tls-certificate-rsa\n\nspec:\nsecretName: mail-tls-certificate-rsa\nisCA: false\nprivateKey:\nalgorithm: RSA\nencoding: PKCS1\nsize: 2048\ndnsNames: [mail.example.com]\nissuerRef:\nname: mail-issuer\nkind: Issuer\n

Attention

You will need to have cert-manager configured. Especially the issue will need to be configured. Since we do not know how you want or need your certificates to be supplied, we do not provide more configuration here. The documentation for cert-manager is excellent.

"},{"location":"config/advanced/kubernetes/#sensitive-data","title":"Sensitive Data","text":"

Sensitive Data

For storing OpenDKIM keys, TLS certificates or any sort of sensitive data, you should be using Secrets. You can mount secrets like ConfigMaps and use them the same way.

The TLS docs page provides guidance when it comes to certificates and transport layer security. Always provide sensitive information vai Secrets.

"},{"location":"config/advanced/kubernetes/#exposing-your-mail-server-to-the-outside-world","title":"Exposing your Mail Server to the Outside World","text":"

The more difficult part with Kubernetes is to expose a deployed DMS to the outside world. Kubernetes provides multiple ways for doing that; each has downsides and complexity. The major problem with exposing DMS to outside world in Kubernetes is to preserve the real client IP. The real client IP is required by DMS for performing IP-based SPF checks and spam checks. If you do not require SPF checks for incoming mails, you may disable them in your Postfix configuration by dropping the line that states: check_policy_service unix:private/policyd-spf.

The easiest approach was covered above, using externalTrafficPolicy: Local, which disables the service proxy, but makes the service local as well (which does not scale). This approach only works when you are given the correct (that is, a public and routable) IP address by a load balancer (like MetalLB). In this sense, the approach above is similar to the next example below. We want to provide you with a few alternatives too. But we also want to communicate the idea of another simple method: you could use a load-balancer without an external IP and DNAT the network traffic to the mail server. After all, this does not interfere with SPF checks because it keeps the origin IP address. If no dedicated external IP address is available, you could try the latter approach, if one is available, use the former.

"},{"location":"config/advanced/kubernetes/#external-ips-service","title":"External IPs Service","text":"

The simplest way is to expose DMS as a Service with external IPs. This is very similar to the approach taken above. Here, an external IP is given to the service directly by you. With the approach above, you tell your load-balancer to do this.

---\napiVersion: v1\nkind: Service\n\nmetadata:\nname: mailserver\nlabels:\napp: mailserver\n\nspec:\nselector:\napp: mailserver\nports:\n- name: smtp\nport: 25\ntargetPort: smtp\n# ...\n\nexternalIPs:\n- 80.11.12.10\n

This approach

  • does not preserve the real client IP, so SPF check of incoming mail will fail.
  • requires you to specify the exposed IPs explicitly.
"},{"location":"config/advanced/kubernetes/#proxy-port-to-service","title":"Proxy port to Service","text":"

The proxy pod helps to avoid the necessity of specifying external IPs explicitly. This comes at the cost of complexity; you must deploy a proxy pod on each Node you want to expose DMS on.

This approach

  • does not preserve the real client IP, so SPF check of incoming mail will fail.
"},{"location":"config/advanced/kubernetes/#bind-to-concrete-node-and-use-host-network","title":"Bind to concrete Node and use host network","text":"

One way to preserve the real client IP is to use hostPort and hostNetwork: true. This comes at the cost of availability; you can reach DMS from the outside world only via IPs of Node where DMS is deployed.

---\napiVersion: extensions/v1beta1\nkind: Deployment\n\nmetadata:\nname: mailserver\n\n# ...\nspec:\nhostNetwork: true\n\n# ...\ncontainers:\n# ...\nports:\n- name: smtp\ncontainerPort: 25\nhostPort: 25\n- name: smtp-auth\ncontainerPort: 587\nhostPort: 587\n- name: imap-secure\ncontainerPort: 993\nhostPort: 993\n#  ...\n

With this approach,

  • it is not possible to access DMS via other cluster Nodes, only via the Node DMS was deployed at.
  • every Port within the Container is exposed on the Host side.
"},{"location":"config/advanced/kubernetes/#proxy-port-to-service-via-proxy-protocol","title":"Proxy Port to Service via PROXY Protocol","text":"

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

"},{"location":"config/advanced/kubernetes/#configure-your-ingress","title":"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\"\n465: \"mailserver/mailserver:465::PROXY\"\n587: \"mailserver/mailserver:587::PROXY\"\n993: \"mailserver/mailserver:993::PROXY\"\n

HAProxy

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

"},{"location":"config/advanced/kubernetes/#configure-the-mailserver","title":"Configure the Mailserver","text":"

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

HAProxy Example
kind: ConfigMap\napiVersion: v1\nmetadata:\nname: mailserver.config\nlabels:\napp: mailserver\ndata:\npostfix-main.cf: |\npostscreen_upstream_proxy_protocol = haproxy\npostfix-master.cf: |\nsmtp/inet/postscreen_upstream_proxy_protocol=haproxy\nsubmission/inet/smtpd_upstream_proxy_protocol=haproxy\nsubmissions/inet/smtpd_upstream_proxy_protocol=haproxy\ndovecot.cf: |\n# Assuming your ingress controller is bound to 10.0.0.0/8\nhaproxy_trusted_networks = 10.0.0.0/8, 127.0.0.0/8\nservice imap-login {\ninet_listener imap {\nhaproxy = yes\n}\ninet_listener imaps {\nhaproxy = yes\n}\n}\n# ...\n---\n\nkind: Deployment\napiVersion: extensions/v1beta1\nmetadata:\nname: mailserver\nspec:\ntemplate:\nspec:\ncontainers:\n- name: docker-mailserver\nvolumeMounts:\n- name: config\nsubPath: postfix-main.cf\nmountPath: /tmp/docker-mailserver/postfix-main.cf\nreadOnly: true\n- name: config\nsubPath: postfix-master.cf\nmountPath: /tmp/docker-mailserver/postfix-master.cf\nreadOnly: true\n- name: config\nsubPath: dovecot.cf\nmountPath: /tmp/docker-mailserver/dovecot.cf\nreadOnly: true\n

With this approach,

  • it is not possible to access DMS via cluster-DNS, as the PROXY protocol is required for incoming connections.
"},{"location":"config/advanced/mail-fetchmail/","title":"Advanced | Email Gathering with Fetchmail","text":"

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

environment:\n- ENABLE_FETCHMAIL=1\n- FETCHMAIL_POLL=300\n

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

\u251c\u2500\u2500 docker-data/dms/config\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 dovecot.cf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 fetchmail.cf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 postfix-accounts.cf\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 postfix-virtual.cf\n\u251c\u2500\u2500 compose.yaml\n\u2514\u2500\u2500 README.md\n
"},{"location":"config/advanced/mail-fetchmail/#configuration","title":"Configuration","text":"

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

"},{"location":"config/advanced/mail-fetchmail/#imap-configuration","title":"IMAP Configuration","text":"

Example

poll 'imap.gmail.com' proto imap\n  user 'username'\n  pass 'secret'\n  is 'user1@example.com'\n  ssl\n
"},{"location":"config/advanced/mail-fetchmail/#pop3-configuration","title":"POP3 Configuration","text":"

Example

poll 'pop3.gmail.com' proto pop3\n  user 'username'\n  pass 'secret'\n  is 'user2@example.com'\n  ssl\n

Caution

Don\u2019t forget the last line! (eg: is 'user1@example.com'). After is, you have to specify an email address from the configuration file: docker-data/dms/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.

"},{"location":"config/advanced/mail-fetchmail/#polling-interval","title":"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:\n- FETCHMAIL_POLL=60\n

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

"},{"location":"config/advanced/mail-fetchmail/#debugging","title":"Debugging","text":"

To debug your fetchmail.cf configuration run this command:

./setup.sh debug fetchmail\n

For more information 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\nTrying to connect to 132.245.48.18/995...connected.\nfetchmail: Server certificate:\nfetchmail: Issuer Organization: Microsoft Corporation\nfetchmail: Issuer CommonName: Microsoft IT SSL SHA2\nfetchmail: Subject CommonName: outlook.com\nfetchmail: Subject Alternative Name: outlook.com\nfetchmail: Subject Alternative Name: *.outlook.com\nfetchmail: Subject Alternative Name: office365.com\nfetchmail: Subject Alternative Name: *.office365.com\nfetchmail: Subject Alternative Name: *.live.com\nfetchmail: Subject Alternative Name: *.internal.outlook.com\nfetchmail: Subject Alternative Name: *.outlook.office365.com\nfetchmail: Subject Alternative Name: outlook.office.com\nfetchmail: Subject Alternative Name: attachment.outlook.office.net\nfetchmail: Subject Alternative Name: attachment.outlook.officeppe.net\nfetchmail: Subject Alternative Name: *.office.com\nfetchmail: outlook.office365.com key fingerprint: 3A:A4:58:42:56:CD:BD:11:19:5B:CF:1E:85:16:8E:4D\nfetchmail: POP3< +OK The Microsoft Exchange POP3 service is ready. [SABFADEAUABSADAAMQBDAEEAMAAwADAANwAuAGUAdQByAHAAcgBkADAAMQAuAHAAcgBvAGQALgBlAHgAYwBoAGEAbgBnAGUAbABhAGIAcwAuAGMAbwBtAA==]\nfetchmail: POP3> CAPA\nfetchmail: POP3< +OK\nfetchmail: POP3< TOP\nfetchmail: POP3< UIDL\nfetchmail: POP3< SASL PLAIN\nfetchmail: POP3< USER\nfetchmail: POP3< .\nfetchmail: POP3> USER user1@outlook.com\nfetchmail: POP3< +OK\nfetchmail: POP3> PASS *\nfetchmail: POP3< +OK User successfully logged on.\nfetchmail: POP3> STAT\nfetchmail: POP3< +OK 0 0\nfetchmail: No mail for user1@outlook.com at outlook.office365.com\nfetchmail: POP3> QUIT\nfetchmail: POP3< +OK Microsoft Exchange Server 2016 POP3 server signing off.\nfetchmail: 6.3.26 querying outlook.office365.com (protocol POP3) at Mon Aug 29 22:11:11 2016: poll completed\nfetchmail: normal termination, status 1\n
"},{"location":"config/advanced/mail-getmail/","title":"Advanced | Email Gathering with Getmail","text":"

To enable the getmail service to retrieve e-mails set the environment variable ENABLE_GETMAIL to 1. Your compose.yaml file should include the following:

environment:\n- ENABLE_GETMAIL=1\n- GETMAIL_POLL=5\n

In your DMS config volume (eg: docker-data/dms/config/), create a getmail-<ID>.cf file for each remote account that you want to retrieve mail and store into a local DMS account. <ID> should be replaced by you, and is just the rest of the filename (eg: getmail-example.cf). The contents of each file should be configuration like documented below.

The directory structure should similar to this:

\u251c\u2500\u2500 docker-data/dms/config\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 dovecot.cf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 getmail-example.cf\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 postfix-accounts.cf\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 postfix-virtual.cf\n\u251c\u2500\u2500 docker-compose.yml\n\u2514\u2500\u2500 README.md\n
"},{"location":"config/advanced/mail-getmail/#configuration","title":"Configuration","text":"

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

"},{"location":"config/advanced/mail-getmail/#common-options","title":"Common Options","text":"

The default options added to each getmail config are:

[options]\nverbose = 0\nread_all = false\ndelete = false\nmax_messages_per_session = 500\nreceived = false\ndelivered_to = false\n

If you want to use a different base config, mount a file to /etc/getmailrc_general. This file will replace the default \"Common Options\" base config above, that all getmail-<ID>.cf files will extend with their configs when used.

IMAP Configuration

This example will:

  1. Connect to the remote IMAP server from Gmail.
  2. Retrieve mail from the gmail account alice with password notsecure.
  3. Store any mail retrieved from the remote mail-server into DMS for the user1@example.com account that DMS manages.
[retriever]\ntype = SimpleIMAPRetriever\nserver = imap.gmail.com\nusername = alice\npassword = notsecure\n[destination]\ntype = MDA_external\npath = /usr/lib/dovecot/deliver\nallow_root_commands = true\narguments =(\"-d\",\"user1@example.com\")\n
POP3 Configuration

Just like the IMAP example above, but instead via POP3 protocol if you prefer that over IMAP.

[retriever]\ntype = SimplePOP3Retriever\nserver = pop3.gmail.com\nusername = alice\npassword = notsecure\n[destination]\ntype = MDA_external\npath = /usr/lib/dovecot/deliver\nallow_root_commands = true\narguments =(\"-d\",\"user1@example.com\")\n
"},{"location":"config/advanced/mail-getmail/#polling-interval","title":"Polling Interval","text":"

By default the getmail service checks external mail accounts for new mail every 5 minutes. That polling interval is configurable via the GETMAIL_POLL ENV variable, with a value in minutes (default: 5, min: 1, max: 30):

environment:\n- GETMAIL_POLL=1\n
"},{"location":"config/advanced/mail-getmail/#xoauth2-authentication","title":"XOAUTH2 Authentication","text":"

It is possible to utilize the getmail-gmail-xoauth-tokens helper to provide authentication using xoauth2 for gmail (example 12) or Microsoft Office 365 (example 13)

"},{"location":"config/advanced/mail-sieve/","title":"Advanced | Email Filtering with Sieve","text":""},{"location":"config/advanced/mail-sieve/#user-defined-sieve-filters","title":"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 docker-data/dms/config/before.dovecot.sieve or a docker-data/dms/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/example.com/user1/home/.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 docker-data/dms/config/ path for each user login that needs a filter. The file name provided should be in the form <user_login>.dovecot.sieve, so for example for user1@example.com you should provide a Sieve file named docker-data/dms/config/user1@example.com.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\"];\n\nif address :contains [\"From\"] \"spam@spam.com\" {\n  fileinto \"INBOX.spam\";\n} else {\n  keep;\n}\n

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\"];\n\nredirect :copy \"user2@not-example.com\";\n

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

Example

redirect \"user2@not-example.com\";\n

You can also use external programs to filter or pipe (process) messages by adding executable scripts in docker-data/dms/config/sieve-pipe or docker-data/dms/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\"];\npipe \"external-program\";\n

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.

"},{"location":"config/advanced/mail-sieve/#automatic-sorting-based-on-subaddresses","title":"Automatic Sorting Based on Subaddresses","text":"

It is possible to sort subaddresses such as user+mailing-lists@example.com into a corresponding folder (here: INBOX/Mailing-lists) automatically.

require [\"envelope\", \"fileinto\", \"mailbox\", \"subaddress\", \"variables\"];\n\nif envelope :detail :matches \"to\" \"*\" {\n  set :lower :upperfirst \"tag\" \"${1}\";\n  if mailboxexists \"INBOX.${1}\" {\n    fileinto \"INBOX.${1}\";\n  } else {\n    fileinto :create \"INBOX.${tag}\";\n  }\n}\n
"},{"location":"config/advanced/mail-sieve/#manage-sieve","title":"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

# compose.yaml\nports:\n- \"4190:4190\"\nenvironment:\n- ENABLE_MANAGESIEVE=1\n

All user defined sieve scripts that are managed by ManageSieve are stored in the user's home folder in /var/mail/example.com/user1/home/sieve. Just one Sieve script might be active for a user and is sym-linked to /var/mail/example.com/user1/home/.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 backed up 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.
"},{"location":"config/advanced/optional-config/","title":"Advanced | Optional Configuration","text":"

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

"},{"location":"config/advanced/optional-config/#directories","title":"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 if SSL_TYPE is set to self-signed or custom. (Docs: SSL)
  • rspamd: Override directory for custom settings when using Rspamd (Docs: Rspamd)
"},{"location":"config/advanced/optional-config/#files","title":"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 <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)
  • rspamd/custom-commands.conf: list of simple commands to adjust Rspamd modules in an easy way (Docs: Rspamd)
"},{"location":"config/advanced/podman/","title":"Advanced | Podman","text":""},{"location":"config/advanced/podman/#introduction","title":"Introduction","text":"

Podman is a daemonless container engine for developing, managing, and running OCI Containers on your Linux System.

About Support for Podman

Please note that Podman is not officially supported as DMS is built and verified on top of the Docker Engine. This content is entirely community supported. If you find errors, please open an issue and provide a PR.

About this Guide

This guide was tested with Fedora 34 using systemd and firewalld. Moreover, it requires Podman version >= 3.2. You may be able to substitute dnf - Fedora's package manager - with others such as apt.

About Security

Running podman in rootless mode requires additional modifications in order to keep your mailserver secure. Make sure to read the related documentation.

"},{"location":"config/advanced/podman/#installation-in-rootfull-mode","title":"Installation in Rootfull Mode","text":"

While using Podman, you can just manage docker-mailserver as what you did with Docker. Your best friend setup.sh includes the minimum code in order to support Podman since it's 100% compatible with the Docker CLI.

The installation is basically the same. Podman v3.2 introduced a RESTful API that is 100% compatible with the Docker API, so you can use Docker Compose with Podman easily. Install Podman and Docker Compose with your package manager first.

sudo dnf install podman docker-compose\n

Then enable podman.socket using systemctl.

systemctl enable --now podman.socket\n

This will create a unix socket locate under /run/podman/podman.sock, which is the entrypoint of Podman's API. Now, configure docker-mailserver and start it.

export DOCKER_HOST=\"unix:///run/podman/podman.sock\"\ndocker compose up -d mailserver\ndocker compose ps\n

You should see that docker-mailserver is running now.

"},{"location":"config/advanced/podman/#self-start-in-rootfull-mode","title":"Self-start in Rootfull Mode","text":"

Podman is daemonless, that means if you want docker-mailserver self-start while boot up the system, you have to generate a systemd file with Podman CLI.

podman generate systemd mailserver > /etc/systemd/system/mailserver.service\nsystemctl daemon-reload\nsystemctl enable --now mailserver.service\n
"},{"location":"config/advanced/podman/#installation-in-rootless-mode","title":"Installation in Rootless Mode","text":"

Running rootless containers is one of Podman's major features. But due to some restrictions, deploying docker-mailserver in rootless mode is not as easy compared to rootfull mode.

  • a rootless container is running in a user namespace so you cannot bind ports lower than 1024
  • a rootless container's systemd file can only be placed in folder under ~/.config
  • a rootless container can result in an open relay, make sure to read the security section.

Also notice that Podman's rootless mode is not about running as a non-root user inside the container, but about the mapping of (normal, non-root) host users to root inside the container.

Warning

In order to make rootless DMS work we must modify some settings in the Linux system, it requires some basic linux server knowledge so don't follow this guide if you not sure what this guide is talking about. Podman rootfull mode and Docker are still good and security enough for normal daily usage.

First, enable podman.socket in systemd's userspace with a non-root user.

systemctl enable --now --user podman.socket\n

The socket file should be located at /var/run/user/$(id -u)/podman/podman.sock. Then, modify compose.yaml to make sure all ports are bindings are on non-privileged ports.

services:\nmailserver:\nports:\n- \"10025:25\"   # SMTP  (explicit TLS => STARTTLS)\n- \"10143:143\"  # IMAP4 (explicit TLS => STARTTLS)\n- \"10465:465\"  # ESMTP (implicit TLS)\n- \"10587:587\"  # ESMTP (explicit TLS => STARTTLS)\n- \"10993:993\"  # IMAP4 (implicit TLS)\n

Then, setup your mailserver.env file follow the documentation and use Docker Compose to start the container.

export DOCKER_HOST=\"unix:///var/run/user/$(id -u)/podman/podman.sock\"\ndocker compose up -d mailserver\ndocker compose ps\n
"},{"location":"config/advanced/podman/#security-in-rootless-mode","title":"Security in Rootless Mode","text":"

In rootless mode, podman resolves all incoming IPs as localhost, which results in an open gateway in the default configuration. There are two workarounds to fix this problem, both of which have their own drawbacks.

"},{"location":"config/advanced/podman/#enforce-authentication-from-localhost","title":"Enforce authentication from localhost","text":"

The PERMIT_DOCKER variable in the mailserver.env file allows to specify trusted networks that do not need to authenticate. If the variable is left empty, only requests from localhost and the container IP are allowed, but in the case of rootless podman any IP will be resolved as localhost. Setting PERMIT_DOCKER=none enforces authentication also from localhost, which prevents sending unauthenticated emails.

"},{"location":"config/advanced/podman/#use-the-slip4netns-network-driver","title":"Use the slip4netns network driver","text":"

The second workaround is slightly more complicated because the compose.yaml has to be modified. As shown in the fail2ban section the slirp4netns network driver has to be enabled. This network driver enables podman to correctly resolve IP addresses but it is not compatible with user defined networks which might be a problem depending on your setup.

Rootless Podman requires adding the value slirp4netns:port_handler=slirp4netns to the --network CLI option, or network_mode setting in your compose.yaml.

You must also add the ENV NETWORK_INTERFACE=tap0, because Podman uses a hard-coded interface name for slirp4netns.

Example

services:\nmailserver:\nnetwork_mode: \"slirp4netns:port_handler=slirp4netns\"\nenvironment:\n- NETWORK_INTERFACE=tap0\n...\n

Note

podman-compose is not compatible with this configuration.

"},{"location":"config/advanced/podman/#self-start-in-rootless-mode","title":"Self-start in Rootless Mode","text":"

Generate a systemd file with the Podman CLI.

podman generate systemd mailserver > ~/.config/systemd/user/mailserver.service\nsystemctl --user daemon-reload\nsystemctl enable --user --now mailserver.service\n

Systemd's user space service is only started when a specific user logs in and stops when you log out. In order to make it to start with the system, we need to enable linger with loginctl

loginctl enable-linger <username>\n

Remember to run this command as root user.

"},{"location":"config/advanced/podman/#port-forwarding","title":"Port Forwarding","text":"

When it comes to forwarding ports using firewalld, see https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/securing_networks/using-and-configuring-firewalld_securing-networks#port-forwarding_using-and-configuring-firewalld for more information.

firewall-cmd --permanent --add-forward-port=port=<25|143|465|587|993>:proto=<tcp>:toport=<10025|10143|10465|10587|10993>\n...\n\n# After you set all ports up.\nfirewall-cmd --reload\n

Notice that this will only open the access to the external client. If you want to access privileges port in your server, do this:

firewall-cmd --permanent --direct --add-rule <ipv4|ipv6> nat OUTPUT 0 -p <tcp|udp> -o lo --dport <25|143|465|587|993> -j REDIRECT --to-ports <10025|10143|10465|10587|10993>\n...\n# After you set all ports up.\nfirewall-cmd --reload\n

Just map all the privilege port with non-privilege port you set in compose.yaml before as root user.

"},{"location":"config/advanced/mail-forwarding/aws-ses/","title":"Mail Forwarding | AWS SES","text":"

Amazon SES (Simple Email Service) is intended to provide a simple way for cloud based applications to send email and receive email. For the purposes of this project only sending email via SES is supported. Older versions of docker-mailserver used AWS_SES_HOST and AWS_SES_USERPASS to configure sending, this has changed and the setup is managed through Configure Relay Hosts.

You will need to create some Amazon SES SMTP credentials. The SMTP credentials you create will be used to populate the RELAY_USER and RELAY_PASSWORD environment variables.

The RELAY_HOST should match your AWS SES region, the RELAY_PORT will be 587.

If all of your email is being forwarded through AWS SES, DEFAULT_RELAY_HOST should be set accordingly.

Example:

DEFAULT_RELAY_HOST=[email-smtp.us-west-2.amazonaws.com]:587\n

Note

If you set up AWS Easy DKIM you can safely skip setting up DKIM as the AWS SES will take care of signing your outgoing email.

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:\nTLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)\nMay 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,\ndelay=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)\n
"},{"location":"config/advanced/mail-forwarding/relay-hosts/","title":"Mail Forwarding | Relay Hosts","text":""},{"location":"config/advanced/mail-forwarding/relay-hosts/#introduction","title":"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.

"},{"location":"config/advanced/mail-forwarding/relay-hosts/#basic-configuration","title":"Basic Configuration","text":"

Basic configuration is done via environment variables:

  • RELAY_HOST: default host to relay mail through, empty (aka '', or no ENV set) 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.

"},{"location":"config/advanced/mail-forwarding/relay-hosts/#advanced-configuration","title":"Advanced Configuration","text":""},{"location":"config/advanced/mail-forwarding/relay-hosts/#sender-dependent-authentication","title":"Sender-dependent Authentication","text":"

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

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

An example configuration file looks like this:

@domain1.com           relay_user_1:password_1\n@domain2.com           relay_user_2:password_2\n

If there is no other configuration, this will cause Postfix to deliver email through 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.

"},{"location":"config/advanced/mail-forwarding/relay-hosts/#sender-dependent-relay-host","title":"Sender-dependent Relay Host","text":"

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

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

An example configuration file looks like this:

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

Combined with the previous configuration in docker-data/dms/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

"},{"location":"config/advanced/mail-forwarding/relay-hosts/#excluding-sender-domains","title":"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 docker-data/dms/config/postfix-relaymap.cf with no destination. You can also do this via:

setup.sh relay exclude-domain <domain>\n

Extending the configuration file from above:

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

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

"},{"location":"config/advanced/maintenance/update-and-cleanup/","title":"Maintenance | Update and Cleanup","text":"

containrrr/watchtower is a service that monitors Docker images for updates, automatically applying them to running containers.

Automatic image updates + cleanup

Run a watchtower container with access to docker.sock, enabling the service to manage Docker:

compose.yaml
services:\nwatchtower:\nimage: containrrr/watchtower:latest\n# Automatic cleanup (removes older image pulls from wasting disk space):\nenvironment:\n- WATCHTOWER_CLEANUP=true\nvolumes:\n- /var/run/docker.sock:/var/run/docker.sock\n

The image tag used for a container is monitored for updates (eg: :latest, :edge, :13)

The automatic update support is only for updates to that specific image tag.

  • Your container will not update to a new major version tag (unless using :latest).
  • Omit the minor or patch portion of the semver tag to receive updates for the omitted portion (eg: 13 will represent the latest minor + patch release of v13).

Updating only specific containers

By default the watchtower service will check every 24 hours for new image updates to pull, based on currently running containers (not restricted to only those running within your compose.yaml).

Images eligible for updates can configured with a custom command that provides a list of container names, or via other supported options (eg: labels). This configuration is detailed in the watchtower docs.

Manual cleanup

watchtower also supports running on-demand with docker run or compose.yaml via the --run-once option.

You can alternatively invoke cleanup of Docker storage directly with:

  • docker image prune --all
  • docker system prune --all (also removes unused containers, networks, build cache).

If you omit the --all option, this will instead only remove \"dangling\" content (eg: Orphaned images).

"},{"location":"config/advanced/override-defaults/dovecot/","title":"Override the Default Configs | Dovecot","text":""},{"location":"config/advanced/override-defaults/dovecot/#add-configuration","title":"Add Configuration","text":"

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

Your DMS folder structure should look like this example:

\u251c\u2500\u2500 docker-data/dms/config\n\u2502   \u251c\u2500\u2500 dovecot.cf\n\u2502   \u251c\u2500\u2500 postfix-accounts.cf\n\u2502   \u2514\u2500\u2500 postfix-virtual.cf\n\u251c\u2500\u2500 compose.yaml\n\u2514\u2500\u2500 README.md\n

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

mail_max_userip_connections = 100\n

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 DMS with multiple end devices.

"},{"location":"config/advanced/override-defaults/dovecot/#override-configuration","title":"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.

services:\nmailserver:\nvolumes:\n- ./docker-data/dms/mail-data/:/var/mail/\n- ./docker-data/dms/config/dovecot/10-master.conf:/etc/dovecot/conf.d/10-master.conf\n

You will first need to obtain the configuration from the running container (where mailserver is the container name):

mkdir -p ./docker-data/dms/config/dovecot\ndocker cp mailserver:/etc/dovecot/conf.d/10-master.conf ./docker-data/dms/config/dovecot/10-master.conf\n
"},{"location":"config/advanced/override-defaults/dovecot/#debugging","title":"Debugging","text":"

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 DMS repository. Make sure to use the one matching your image version release.

The file docker-data/dms/config/dovecot.cf is copied internally to /etc/dovecot/local.conf. To verify the file content, run:

docker exec -it mailserver cat /etc/dovecot/local.conf\n
"},{"location":"config/advanced/override-defaults/postfix/","title":"Override the Default Configs | Postfix","text":"

Our default Postfix configuration can easily be extended to add parameters or modify existing ones by providing a docker-data/dms/config/postfix-main.cf. This file uses the same format as Postfix main.cf does (See official docs for all parameters and syntax rules).

Example

One can easily increase the backwards-compatibility level and set new Postscreen options:

# increase the compatibility level from 2 (default) to 3\ncompatibility_level = 3\n# set a threshold value for Spam detection\npostscreen_dnsbl_threshold = 4\n

How are your changes applied?

The custom configuration you supply is appended to the default configuration located at /etc/postfix/main.cf, and then postconf -nf is run to remove earlier duplicate entries that have since been replaced. This happens early during container startup before Postfix is started.

Similarly, it is possible to add a custom docker-data/dms/config/postfix-master.cf file that will override the standard master.cf. Note: Each line in this file will be passed to postconf -P, i.e. the file is not appended as a whole to /etc/postfix/master.cf like docker-data/dms/config/postfix-main.cf! The expected format is <service_name>/<type>/<parameter>, for example:

# adjust the submission \"reject_unlisted_recipient\" option\nsubmission/inet/smtpd_reject_unlisted_recipient=no\n

Attention

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

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

"},{"location":"config/advanced/override-defaults/user-patches/","title":"Custom User Changes & Patches | Scripting","text":"

If you'd like to change, patch or alter files or behavior of DMS, 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 docker-data/dms/config/user-patches.sh in order to create the user-patches.sh script.

If you are managing your directory structure yourself, create a docker-data/dms/config/ directory and add the user-patches.sh file yourself.

# 1. Either create the docker-data/dms/config/ directory yourself\n#    or let docker-mailserver create it on initial startup\n/tmp $ mkdir -p docker-data/dms/config/ && cd docker-data/dms/config/\n\n# 2. Create the user-patches.sh file and edit it\n/tmp/docker-data/dms/config $ touch user-patches.sh\n/tmp/docker-data/dms/config $ nano user-patches.sh\n

The contents could look like this:

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

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.

"},{"location":"config/best-practices/autodiscover/","title":"Auto-Discovery of Services","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 URI. 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.

"},{"location":"config/best-practices/dkim_dmarc_spf/","title":"DKIM, DMARC & SPF","text":"

Cloudflare has written an article about DKIM, DMARC and SPF that we highly recommend you to read to get acquainted with the topic.

Rspamd vs Individual validators

With v12.0.0, Rspamd was integrated into DMS. It can perform validations for DKIM, DMARC and SPF as part of the spam-score-calculation for an email. DMS provides individual alternatives for each validation that can be used instead of deferring to Rspamd:

  • DKIM: opendkim is used as a milter (like Rspamd)
  • DMARC: opendmarc is used as a milter (like Rspamd)
  • SPF: policyd-spf is used in Postfix's smtpd_recipient_restrictions

In a future release Rspamd will become the default for these validations, with a deprecation notice issued prior to the removal of the above alternatives.

We encourage everyone to prefer Rspamd via ENABLE_RSPAMD=1.

DNS Caches & Propagation

While modern DNS providers are quick, it may take minutes or even hours for new DNS records to become available / propagate.

"},{"location":"config/best-practices/dkim_dmarc_spf/#dkim","title":"DKIM","text":"

What is DKIM

DomainKeys Identified Mail (DKIM) is an email authentication method designed to detect forged sender addresses in email (email spoofing), a technique often used in phishing and email spam.

Source

When DKIM is enabled:

  1. Inbound mail will verify any included DKIM signatures
  2. Outbound mail is signed (when you're sending domain has a configured DKIM key)

DKIM requires a public/private key pair to enable signing (via private key) your outgoing mail, while the receiving end must query DNS to verify (via public key) that the signature is trustworthy.

"},{"location":"config/best-practices/dkim_dmarc_spf/#generating-keys","title":"Generating Keys","text":"

You'll need to repeat this process if you add any new domains.

You should have:

  • At least one email account setup
  • Attached a volume for config to persist the generated files to local storage

Creating DKIM Keys

DKIM keys can be generated with good defaults by running:

docker exec -it <CONTAINER NAME> setup config dkim\n

If you need to generate your keys with different settings, check the help output for supported config options and examples:

docker exec -it <CONTAINER NAME> setup config dkim help\n

As described by the help output, you may need to use the domain option explicitly when you're using LDAP or Rspamd.

Changing the key size

The keypair generated for using with DKIM presently defaults to RSA-2048. This is a good size but you can lower the security to 1024-bit, or increase it to 4096-bit (discouraged as that is excessive).

To generate a key with different size (for RSA 1024-bit) run:

setup config dkim keysize 1024\n

RSA Key Sizes >= 4096 Bit

According to RFC 8301, keys are preferably between 1024 and 2048 bits. Keys of size 4096-bit or larger may not be compatible to all systems your mail is intended for.

You should not need a key length beyond 2048-bit. If 2048-bit does not meet your security needs, you may want to instead consider adopting key rotation or switching from RSA to ECC keys for DKIM.

You may need to specify mail domains explicitly

Required when using LDAP and Rspamd.

setup config dkim will generate DKIM keys for what is assumed as the primary mail domain (derived from the FQDN assigned to DMS, minus any subdomain).

When the DMS FQDN is mail.example.com or example.com, by default this command will generate DKIM keys for example.com as the primary domain for your users mail accounts (eg: hello@example.com).

The DKIM generation does not have support to query LDAP for additionanl mail domains it should know about. If the primary mail domain is not sufficient, then you must explicitly specify any extra domains via the domain option:

# ENABLE_OPENDKIM=1 (default):\nsetup config dkim domain 'example.com,another-example.com'\n\n# ENABLE_RSPAMD=1 + ENABLE_OPENDKIM=0:\nsetup config dkim domain example.com\nsetup config dkim domain another-example.com\n

OpenDKIM with ACCOUNT_PROVISIONER=FILE

When DMS uses this configuration, it will by default also detect mail domains (from accounts added via setup email add), generating additional DKIM keys.

DKIM is currently supported by either OpenDKIM or Rspamd:

OpenDKIMRspamd

OpenDKIM is currently enabled by default.

After running setup config dkim, your new DKIM key files (and OpenDKIM config) have been added to /tmp/docker-mailserver/opendkim/.

Restart required

After restarting DMS, outgoing mail will now be signed with your new DKIM key(s)

Requires opt-in via ENABLE_RSPAMD=1 (and disable the default OpenDKIM: ENABLE_OPENDKIM=0).

Rspamd provides DKIM support through two separate modules:

  1. Verifying DKIM signatures from inbound mail is enabled by default.
  2. Signing outbound mail with your DKIM key needs additional setup (key + dns + config).
Using Multiple Domains

If you have multiple domains, you need to:

  • Create a key wth docker exec -it <CONTAINER NAME> setup config dkim domain <DOMAIN> for each domain DMS should sign outgoing mail for.
  • Provide a custom dkim_signing.conf (for which an example is shown below), as the default config only supports one domain.

About the Helper Script

The script will persist the keys in /tmp/docker-mailserver/rspamd/dkim/. Hence, if you are already using the default volume mounts, the keys are persisted in a volume. The script also restarts Rspamd directly, so changes take effect without restarting DMS.

The script provides you with log messages along the way of creating keys. In case you want to read the complete log, use -v (verbose) or -vv (very verbose).

In case you have not already provided a default DKIM signing configuration, the script will create one and write it to /etc/rspamd/override.d/dkim_signing.conf. If this file already exists, it will not be overwritten.

When you're already using the rspamd/override.d/ directory, the file is created inside your volume and therefore persisted correctly. If you are not using rspamd/override.d/, you will need to persist the file yourself (otherwise it is lost on container restart).

An example of what a default configuration file for DKIM signing looks like can be found by expanding the example below.

DKIM Signing Module Configuration Examples

A simple configuration could look like this:

# documentation: https://rspamd.com/doc/modules/dkim_signing.html\n\nenabled = true;\n\nsign_authenticated = true;\nsign_local = true;\n\nuse_domain = \"header\";\nuse_redis = false; # don't change unless Redis also provides the DKIM keys\nuse_esld = true;\ncheck_pubkey = true; # you want to use this in the beginning\n\ndomain {\nexample.com {\npath = \"/tmp/docker-mailserver/rspamd/dkim/mail.private\";\nselector = \"mail\";\n}\n}\n

As shown next:

  • You can add more domains into the domain { ... } section (in the following example: example.com and example.org).
  • A domain can also be configured with multiple selectors and keys within a selectors [ ... ] array (in the following example, this is done for example.org).
# ...\n\ndomain {\nexample.com {\npath = /tmp/docker-mailserver/rspamd/example.com/ed25519.private\";\nselector = \"dkim-ed25519\";\n}\nexample.org {\nselectors [\n{\npath = \"/tmp/docker-mailserver/rspamd/dkim/example.org/rsa.private\";\nselector = \"dkim-rsa\";\n},\n{\npath = \"/tmp/docker-mailserver/rspamd/dkim/example.org/ed25519.private\";\nselector = \"dkim-ed25519\";\n}\n]\n}\n}\n
Support for DKIM Keys using ED25519

This modern elliptic curve is supported by Rspamd, but support by third-parties for verifying Ed25519 DKIM signatures is unreliable.

If you sign your mail with this key type, you should include RSA as a fallback, like shown in the above example.

Let Rspamd Check Your Keys

When check_pubkey = true; is set, Rspamd will query the DNS record for each DKIM selector, verifying each public key matches the private key configured.

If there is a mismatch, a warning will be emitted to the Rspamd log /var/log/mail/rspamd.log.

"},{"location":"config/best-practices/dkim_dmarc_spf/#dkim-dns","title":"DNS Record","text":"

When mail signed with your DKIM key is sent from your mail server, the receiver needs to check a DNS TXT record to verify the DKIM signature is trustworthy.

Configuring DNS - DKIM record

When you generated your key in the previous step, the DNS data was saved into a file <selector>.txt (default: mail.txt). Use this content to update your DNS via Web Interface or directly edit your DNS Zone file:

Web InterfaceDNS Zone file

Create a new record:

Field Value Type TXT Name <selector>._domainkey (default: mail._domainkey) TTL Use the default (otherwise 3600 seconds is appropriate) Data File content within ( ... ) (formatted as advised below)

When using Rspamd, the helper script has already provided you with the contents (the \"Data\" field) of the DNS record you need to create - you can just copy-paste this text.

<selector>.txt is already formatted as a snippet for adding to your DNS Zone file.

Just copy/paste the file contents into your existing DNS zone. The TXT value has been split into separate strings every 255 characters for compatibility.

<selector>.txt - Formatting the TXT record value correctly

This file was generated for use within a DNS zone file. The file name uses the DKIM selector it was generated with (default DKIM selector is mail, which creates mail.txt_).

For your DNS setup, DKIM support needs to create a TXT record to store the public key for mail clients to use. TXT records with values that are longer than 255 characters need to be split into multiple parts. This is why the generated <selector>.txt file (containing your public key for use with DKIM) has multiple value parts wrapped within double-quotes between ( and ).

A DNS web-interface may handle this separation internally instead, and could expect the value provided all as a single line instead of split. When that is required, you'll need to manually format the value as described below.

Your generated DNS record file (<selector>.txt) should look similar to this:

mail._domainkey IN TXT ( \"v=DKIM1; k=rsa; \"\n\"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ\"\n\"5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB\"\n) ;\n

Take the content between ( ... ), and combine all the quote wrapped content and remove the double-quotes including the white-space between them. That is your TXT record value, the above example would become this:

v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB\n

To test that your new DKIM record is correct, query it with the dig command. The TXT value response should be a single line split into multiple parts wrapped in double-quotes:

$ dig +short TXT mail._domainkey.example.com\n\"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39\" \"KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB\"\n
"},{"location":"config/best-practices/dkim_dmarc_spf/#dkim-debug","title":"Troubleshooting","text":"

MxToolbox has a DKIM Verifier that you can use to check your DKIM DNS record(s).

When using Rspamd, we recommend you turn on check_pubkey = true; in dkim_signing.conf. Rspamd will then check whether your private key matches your public key, and you can check possible mismatches by looking at /var/log/mail/rspamd.log.

"},{"location":"config/best-practices/dkim_dmarc_spf/#dmarc","title":"DMARC","text":"

With DMS, DMARC is pre-configured out of the box. You may disable extra and excessive DMARC checks when using Rspamd via ENABLE_OPENDMARC=0.

The only thing you need to do in order to enable DMARC on a \"DNS-level\" is to add new TXT. In contrast to DKIM, DMARC DNS entries do 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 this one).

Typically something like this should be good to start with:

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

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

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

The DMARC status may not be displayed instantly due to delays in DNS (caches). Dmarcian has a few tools you can use to verify your DNS records.

"},{"location":"config/best-practices/dkim_dmarc_spf/#spf","title":"SPF","text":"

What is SPF

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.

Source

Disabling policyd-spf?

As of now, policyd-spf cannot be disabled. This is WIP.

"},{"location":"config/best-practices/dkim_dmarc_spf/#adding-an-spf-record","title":"Adding an SPF Record","text":"

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

example.com. IN TXT \"v=spf1 mx ~all\"\n

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.

"},{"location":"config/best-practices/dkim_dmarc_spf/#backup-mx-secondary-mx-for-policyd-spf","title":"Backup MX & Secondary MX for policyd-spf","text":"

For whitelisting an 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 at docker-data/dms/config/postfix-policyd-spf.conf:

debugLevel = 1\n#0(only errors)-4(complete data received)\n\nskip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1\n\n# Preferably use IP-Addresses for whitelist lookups:\nWhitelist = 192.168.0.0/31,192.168.1.0/30\n# Domain_Whitelist = mx1.not-example.com,mx2.not-example.com\n

Then add this line to compose.yaml:

volumes:\n- ./docker-data/dms/config/postfix-policyd-spf.conf:/etc/postfix-policyd-spf-python/policyd-spf.conf\n
"},{"location":"config/security/fail2ban/","title":"Security | Fail2Ban","text":"

What is Fail2Ban (F2B)?

Fail2ban is an intrusion prevention software framework. Written in the Python programming language, it is designed to prevent against brute-force attacks. It is able to run on POSIX systems that have an interface to a packet-control system or firewall installed locally, such as [NFTables] or TCP Wrapper.

Source

"},{"location":"config/security/fail2ban/#configuration","title":"Configuration","text":"

Warning

DMS must be launched with the NET_ADMIN capability in order to be able to install the NFTables rules that actually ban IP addresses. Thus, either include --cap-add=NET_ADMIN in the docker run command, or the equivalent in the compose.yaml:

cap_add:\n- NET_ADMIN\n

Running Fail2Ban on Older Kernels

DMS configures F2B to use NFTables, not IPTables (legacy). We have observed that older systems, for example NAS systems, do not support the modern NFTables rules. You will need to configure F2B to use legacy IPTables again, for example with the fail2ban-jail.cf, see the section on configuration further down below.

"},{"location":"config/security/fail2ban/#dms-defaults","title":"DMS Defaults","text":"

DMS will automatically ban IP addresses of hosts that have generated 6 failed attempts over the course of the last week. The bans themselves last for one week. The Postfix jail is configured to use mode = extra in DMS.

"},{"location":"config/security/fail2ban/#custom-files","title":"Custom Files","text":"

What is docker-data/dms/config/?

This following configuration files inside the docker-data/dms/config/ volume will be copied inside the container during startup

  1. fail2ban-jail.cf is copied to /etc/fail2ban/jail.d/user-jail.local
    • with this file, you can adjust the configuration of individual jails and their defaults
    • there is an example provided in our repository on GitHub
  2. fail2ban-fail2ban.cf is copied to /etc/fail2ban/fail2ban.local
    • with this file, you can adjust F2B behavior in general
    • there is an example provided in our repository on GitHub
"},{"location":"config/security/fail2ban/#viewing-all-bans","title":"Viewing All Bans","text":"

When just running

setup fail2ban\n

the script will show all banned IP addresses.

To get a more detailed status view, run

setup fail2ban status\n
"},{"location":"config/security/fail2ban/#managing-bans","title":"Managing Bans","text":"

You can manage F2B with the setup script. The usage looks like this:

docker exec <CONTAINER NAME> setup fail2ban [<ban|unban> <IP>]\n
"},{"location":"config/security/fail2ban/#viewing-the-log-file","title":"Viewing the Log File","text":"
docker exec <CONTAINER NAME> setup fail2ban log\n
"},{"location":"config/security/fail2ban/#running-inside-a-rootless-container","title":"Running Inside A Rootless Container","text":"

RootlessKit is the fakeroot implementation for supporting rootless mode in Docker and Podman. By default, RootlessKit uses the builtin port forwarding driver, which does not propagate source IP addresses.

It is necessary for F2B to have access to the real source IP addresses in order to correctly identify clients. This is achieved by changing the port forwarding driver to slirp4netns, which is slower than the builtin driver but does preserve the real source IPs.

DockerPodman

For rootless mode in Docker, create ~/.config/systemd/user/docker.service.d/override.conf with the following content:

Danger

This changes the port driver for all rootless containers managed by Docker. Per container configuration is not supported, if you need that consider Podman instead.

[Service]\nEnvironment=\"DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns\"\n

And then restart the daemon:

$ systemctl --user daemon-reload\n$ systemctl --user restart docker\n

Rootless Podman requires adding the value slirp4netns:port_handler=slirp4netns to the --network CLI option, or network_mode setting in your compose.yaml:

Example

services:\nmailserver:\nnetwork_mode: \"slirp4netns:port_handler=slirp4netns\"\nenvironment:\n- ENABLE_FAIL2BAN=1\n- NETWORK_INTERFACE=tap0\n...\n

You must also add the ENV NETWORK_INTERFACE=tap0, because Podman uses a hard-coded interface name for slirp4netns. slirp4netns is not compatible with user-defined networks!

"},{"location":"config/security/mail_crypt/","title":"Security | mail_crypt (email/storage encryption)","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/

"},{"location":"config/security/mail_crypt/#single-encryption-key-global-method","title":"Single Encryption Key / Global Method","text":"
  1. Create 10-custom.conf and populate it with the following:

    # Enables mail_crypt for all services (imap, pop3, etc)\nmail_plugins = $mail_plugins mail_crypt\nplugin {\n  mail_crypt_global_private_key = </certs/ecprivkey.pem\n  mail_crypt_global_public_key = </certs/ecpubkey.pem\n  mail_crypt_save_version = 2\n}\n
  2. Shutdown your mailserver (docker compose down)

  3. You then need to generate your global EC key. We named them /certs/ecprivkey.pem and /certs/ecpubkey.pem in step #1.

  4. The EC key needs to be available in the container. I prefer to mount a /certs directory into the container:

    services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\nvolumes:\n. . .\n- ./certs/:/certs\n. . .\n

  5. While you're editing the compose.yaml, add the configuration file:

    services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\nvolumes:\n. . .\n- ./config/dovecot/10-custom.conf:/etc/dovecot/conf.d/10-custom.conf\n- ./certs/:/certs\n. . .\n

  6. Start the container, monitor the logs for any errors, send yourself a message, and then confirm the file on disk is encrypted:

    [root@ip-XXXXXXXXXX ~]# cat -A /mnt/efs-us-west-2/maildata/awesomesite.com/me/cur/1623989305.M6v\ufffdz\ufffd@\ufffd\ufffd m}\ufffd\ufffd,\ufffd\ufffd9\ufffd\ufffd\ufffd\ufffdB*\ufffd247.us-west-2.compute.inE\ufffd\ufffd\\Ck*\ufffd@7795,W=7947:2,\nT\ufffd9\ufffd8t\ufffd6\ufffd\ufffd t\ufffd\ufffd\ufffde\ufffdW\ufffd\ufffdS   `\ufffdH\ufffd\ufffdC\ufffd\u06a4 \ufffdyeY\ufffd\ufffdXZ\ufffd\ufffd^\ufffdd\ufffd/\ufffd\ufffd+\ufffdA\n

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

"},{"location":"config/security/rspamd/","title":"Security | Rspamd","text":""},{"location":"config/security/rspamd/#about","title":"About","text":"

Rspamd is a \"fast, free and open-source spam filtering system\". DMS integrates Rspamd like any other service. We provide a very simple but easy to maintain setup of Rspamd.

If you want to have a look at the default configuration files for Rspamd that DMS packs, navigate to target/rspamd/ inside the repository. Please consult the section \"The Default Configuration\" section down below for a written overview.

"},{"location":"config/security/rspamd/#related-environment-variables","title":"Related Environment Variables","text":"

The following environment variables are related to Rspamd:

  1. ENABLE_RSPAMD
  2. ENABLE_RSPAMD_REDIS
  3. RSPAMD_CHECK_AUTHENTICATED
  4. RSPAMD_GREYLISTING
  5. RSPAMD_HFILTER
  6. RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE
  7. RSPAMD_LEARN
  8. MOVE_SPAM_TO_JUNK
  9. MARK_SPAM_AS_READ

With these variables, you can enable Rspamd itself and you can enable / disable certain features related to Rspamd.

"},{"location":"config/security/rspamd/#the-default-configuration","title":"The Default Configuration","text":""},{"location":"config/security/rspamd/#mode-of-operation","title":"Mode of Operation","text":"

The proxy worker operates in self-scan mode. This simplifies the setup as we do not require a normal worker. You can easily change this though by overriding the configuration by DMS.

DMS does not set a default password for the controller worker. You may want to do that yourself. In setups where you already have an authentication provider in front of the Rspamd webpage, you may want to set the secure_ip option to \"0.0.0.0/0\" for the controller worker to disable password authentication inside Rspamd completely.

"},{"location":"config/security/rspamd/#persistence-with-redis","title":"Persistence with Redis","text":"

When Rspamd is enabled, we implicitly also start an instance of Redis in the container. Redis is configured to persist it's data via RDB snapshots to disk in the directory /var/lib/redis (which is a symbolic link to /var/mail-state/lib-redis/ when ONE_DIR=1 and a volume is mounted to /var/mail-state/). With the volume mount the snapshot will restore the Redis data across container restarts, and provide a way to keep backup.

Redis uses /etc/redis/redis.conf for configuration. We adjust this file when enabling the internal Redis service. If you have an external instance of Redis to use, the internal Redis service can be opt-out via setting the ENV ENABLE_RSPAMD_REDIS=0 (link also details required changes to the DMS rspamd config).

"},{"location":"config/security/rspamd/#web-interface","title":"Web Interface","text":"

Rspamd provides a web interface, which contains statistics and data Rspamd collects. The interface is enabled by default and reachable on port 11334.

"},{"location":"config/security/rspamd/#dns","title":"DNS","text":"

DMS does not supply custom values for DNS servers to Rspamd. If you need to use custom DNS servers, which could be required when using DNS-based black/whitelists, you need to adjust options.inc yourself.

Making DNS Servers Configurable

If you want to see an environment variable (like RSPAMD_DNS_SERVERS) to support custom DNS servers for Rspamd being added to DMS, please raise a feature request issue.

Danger

While we do not provide values for custom DNS servers by default, we set soft_reject_on_timeout = true; by default. This setting will cause a soft reject if a task (presumably a DNS request) timeout takes place.

This setting is enabled to not allow spam to proceed just because DNS requests did not succeed. It could deny legitimate e-mails to pass though too in case your DNS setup is incorrect or not functioning properly.

"},{"location":"config/security/rspamd/#logs","title":"Logs","text":"

You can find the Rspamd logs at /var/log/mail/rspamd.log, and the corresponding logs for Redis, if it is enabled, at /var/log/supervisor/rspamd-redis.log. We recommend inspecting these logs (with docker exec -it <CONTAINER NAME> less /var/log/mail/rspamd.log) in case Rspamd does not work as expected.

"},{"location":"config/security/rspamd/#modules","title":"Modules","text":"

You can find a list of all Rspamd modules on their website.

"},{"location":"config/security/rspamd/#disabled-by-default","title":"Disabled By Default","text":"

DMS disables certain modules (clickhouse, elastic, neural, reputation, spamassassin, url_redirector, metric_exporter) by default. We believe these are not required in a standard setup, and they would otherwise needlessly use system resources.

"},{"location":"config/security/rspamd/#anti-virus-clamav","title":"Anti-Virus (ClamAV)","text":"

You can choose to enable ClamAV, and Rspamd will then use it to check for viruses. Just set the environment variable ENABLE_CLAMAV=1.

"},{"location":"config/security/rspamd/#rbls-realtime-blacklists-dnsbls-dns-based-blacklists","title":"RBLs (Realtime Blacklists) / DNSBLs (DNS-based Blacklists)","text":"

The RBL module is enabled by default. As a consequence, Rspamd will perform DNS lookups to a variety of blacklists. Whether an RBL or a DNSBL is queried depends on where the domain name was obtained: RBL servers are queried with IP addresses extracted from message headers, DNSBL server are queried with domains and IP addresses extracted from the message body [source].

Rspamd and DNS Block Lists

When the RBL module is enabled, Rspamd will do a variety of DNS requests to (amongst other things) DNSBLs. There are a variety of issues involved when using DNSBLs. Rspamd will try to mitigate some of them by properly evaluating all return codes. This evaluation is a best effort though, so if the DNSBL operators change or add return codes, it may take a while for Rspamd to adjust as well.

If you want to use DNSBLs, try to use your own DNS resolver and make sure it is set up correctly, i.e. it should be a non-public & recursive resolver. Otherwise, you might not be able (see this Spamhaus post) to make use of the block lists.

"},{"location":"config/security/rspamd/#providing-custom-settings-overriding-settings","title":"Providing Custom Settings & Overriding Settings","text":"

DMS brings sane default settings for Rspamd. They are located at /etc/rspamd/local.d/ inside the container (or target/rspamd/local.d/ in the repository).

"},{"location":"config/security/rspamd/#manually","title":"Manually","text":"

What is docker-data/dms/config/?

If you want to overwrite the default settings and / or provide your own settings, you can place files at docker-data/dms/config/rspamd/override.d/. Files from this directory are copied to /etc/rspamd/override.d/ during startup. These files forcibly override Rspamd and DMS default settings.

Clashing Overrides

Note that when also using the custom-commands.conf file, files in override.d may be overwritten in case you adjust them manually and with the help of the file.

"},{"location":"config/security/rspamd/#with-the-help-of-a-custom-file","title":"With the Help of a Custom File","text":"

DMS provides the ability to do simple adjustments to Rspamd modules with the help of a single file. Just place a file called custom-commands.conf into docker-data/dms/config/rspamd/. If this file is present, DMS will evaluate it. The structure is very simple. Each line in the file looks like this:

COMMAND ARGUMENT1 ARGUMENT2 ARGUMENT3\n

where COMMAND can be:

  1. disable-module: disables the module with name ARGUMENT1
  2. enable-module: explicitly enables the module with name ARGUMENT1
  3. set-option-for-module: sets the value for option ARGUMENT2 to ARGUMENT3 inside module ARGUMENT1
  4. set-option-for-controller: set the value of option ARGUMENT1 to ARGUMENT2 for the controller worker
  5. set-option-for-proxy: set the value of option ARGUMENT1 to ARGUMENT2 for the proxy worker
  6. set-common-option: set the option ARGUMENT1 that defines basic Rspamd behaviour to value ARGUMENT2
  7. add-line: this will add the complete line after ARGUMENT1 (with all characters) to the file /etc/rspamd/override.d/<ARGUMENT1>

An Example Is Shown Down Below

File Names & Extensions

For command 1 - 3, we append the .conf suffix to the module name to get the correct file name automatically. For commands 4 - 6, the file name is fixed (you don't even need to provide it). For command 7, you will need to provide the whole file name (including the suffix) yourself!

You can also have comments (the line starts with #) and blank lines in custom-commands.conf - they are properly handled and not evaluated.

Adjusting Modules This Way

These simple commands are meant to give users the ability to easily alter modules and their options. As a consequence, they are not powerful enough to enable multi-line adjustments. If you need to do something more complex, we advise to do that manually!

"},{"location":"config/security/rspamd/#examples-advanced-configuration","title":"Examples & Advanced Configuration","text":""},{"location":"config/security/rspamd/#a-very-basic-configuration","title":"A Very Basic Configuration","text":"

You want to start using Rspamd? Rspamd is disabled by default, so you need to set the following environment variables:

ENABLE_RSPAMD=1\nENABLE_OPENDKIM=0\nENABLE_OPENDMARC=0\nENABLE_POLICYD_SPF=0\nENABLE_AMAVIS=0\nENABLE_SPAMASSASSIN=0\n

This will enable Rspamd and disable services you don't need when using Rspamd.

"},{"location":"config/security/rspamd/#adjusting-and-extending-the-very-basic-configuration","title":"Adjusting and Extending The Very Basic Configuration","text":"

Rspamd is running, but you want or need to adjust it? First, create a file named custom-commands.conf under docker-data/dms/config/rspamd (which translates to /tmp/docker-mailserver/rspamd/ inside the container). Then add you changes:

  1. Say you want to be able to easily look at the frontend Rspamd provides on port 11334 (default) without the need to enter a password (maybe because you already provide authorization and authentication). You will need to adjust the controller worker: set-option-for-controller secure_ip \"0.0.0.0/0\".
  2. You additionally want to enable the auto-spam-learning for the Bayes module? No problem: set-option-for-module classifier-bayes autolearn true.
  3. But the chartable module gets on your nerves? Easy: disable-module chartable.
What Does the Result Look Like?

Here is what the file looks like in the end:

# See 1.\n# ATTENTION: this disables authentication on the website - make sure you know what you're doing!\nset-option-for-controller secure_ip \"0.0.0.0/0\"\n\n# See 2.\nset-option-for-module classifier-bayes autolearn true\n\n# See 3.\ndisable-module chartable\n
"},{"location":"config/security/rspamd/#dkim-signing","title":"DKIM Signing","text":"

There is a dedicated section for setting up DKIM with Rspamd in our documentation.

"},{"location":"config/security/rspamd/#abusix-integration","title":"Abusix Integration","text":"

This subsection gives information about the integration of Abusix, \"a set of blocklists that work as an additional email security layer for your existing mail environment\". The setup is straight-forward and well documented:

  1. Create an account
  2. Retrieve your API key
  3. Navigate to the \"Getting Started\" documentation for Rspamd and follow the steps described there
  4. Make sure to change <APIKEY> to your private API key

We recommend mounting the files directly into the container, as they are rather big and not manageable with the modules script. If mounted to the correct location, Rspamd will automatically pick them up.

While Abusix can be integrated into Postfix, Postscreen and a multitude of other software, we recommend integrating Abusix only into a single piece of software running in your mail server - everything else would be excessive and wasting queries. Moreover, we recommend the integration into suitable filtering software and not Postfix itself, as software like Postscreen or Rspamd can properly evaluate the return codes and other configuration.

"},{"location":"config/security/ssl/","title":"Security | TLS (aka SSL)","text":"

There are multiple options to enable SSL (via SSL_TYPE):

  • Using letsencrypt (recommended)
  • Using Caddy
  • Using Traefik
  • Using self-signed certificates
  • Using your own certificates

After installation, you can test your setup with:

  • checktls.com
  • testssl.sh

Exposure of DNS labels through Certificate Transparency

All public Certificate Authorities (CAs) are required to log certificates they issue publicly via Certificate Transparency. This helps to better establish trust.

When using a public CA for certificates used in private networks, be aware that the associated DNS labels in the certificate are logged publicly and easily searchable. These logs are append only, you cannot redact this information.

You could use a wildcard certificate. This avoids accidentally leaking information to the internet, but keep in mind the potential security risks of wildcard certs.

"},{"location":"config/security/ssl/#the-fqdn","title":"The FQDN","text":"

An FQDN (Fully Qualified Domain Name) such as mail.example.com is required for DMS to function correctly, especially for looking up the correct SSL certificate to use.

  • mail.example.com will still use user@example.com as the mail address. You do not need a bare domain for that.
  • We usually discourage assigning a bare domain (When your DNS MX record does not point to a subdomain) to represent DMS. However, an FQDN of just example.com is also supported.
  • Internally, hostname -f will be used to retrieve the FQDN as configured in the below examples.
  • Wildcard certificates (eg: *.example.com) are supported for SSL_TYPE=letsencrypt. Your configured FQDN below may be mail.example.com, and your wildcard certificate provisioned to /etc/letsencrypt/live/example.com which will be checked as a fallback FQDN by DMS.

Setting the hostname correctly

Change mail.example.com below to your own FQDN.

# CLI:\ndocker run --hostname mail.example.com\n

or

# compose.yaml\nservices:\nmailserver:\nhostname: mail.example.com\n
"},{"location":"config/security/ssl/#provisioning-methods","title":"Provisioning methods","text":""},{"location":"config/security/ssl/#lets-encrypt-recommended","title":"Let's Encrypt (Recommended)","text":"

To enable Let's Encrypt for DMS, you have to:

  1. Get your certificate using the Let's Encrypt client Certbot.
  2. For your DMS container:

    • Add the environment variable SSL_TYPE=letsencrypt.
    • Mount your local letsencrypt folder as a volume to /etc/letsencrypt.

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

Note

/etc/letsencrypt/live stores provisioned certificates in individual folders named by their FQDN.

Make sure that the entire folder is mounted to DMS as there are typically symlinks from /etc/letsencrypt/live/mail.example.com to /etc/letsencrypt/archive.

Example

Add these additions to the mailserver service in your compose.yaml:

services:\nmailserver:\nhostname: mail.example.com\nenvironment:\n- SSL_TYPE=letsencrypt\nvolumes:\n- /etc/letsencrypt:/etc/letsencrypt\n
"},{"location":"config/security/ssl/#example-using-docker-for-lets-encrypt","title":"Example using Docker for Let's Encrypt","text":"

Certbot provisions certificates to /etc/letsencrypt. Add a volume to store these, so that they can later be accessed by DMS container. You may also want to persist Certbot logs, just in case you need to troubleshoot.

  1. Getting a certificate is this simple! (Referencing: Certbot docker instructions and certonly --standalone mode):

    # Requires access to port 80 from the internet, adjust your firewall if needed.\ndocker run --rm -it \\\n-v \"${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/\" \\\n-v \"${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/\" \\\n-p 80:80 \\\ncertbot/certbot certonly --standalone -d mail.example.com\n
  2. Add a volume for DMS that maps the local certbot/certs/ folder to the container path /etc/letsencrypt/.

    Example

    Add these additions to the mailserver service in your compose.yaml:

    services:\nmailserver:\nhostname: mail.example.com\nenvironment:\n- SSL_TYPE=letsencrypt\nvolumes:\n- ./docker-data/certbot/certs/:/etc/letsencrypt\n
  3. The certificate setup is complete, but remember it will expire. Consider automating renewals.

Renewing Certificates

When running the above certonly --standalone snippet again, the existing certificate is renewed if it would expire within 30 days.

Alternatively, Certbot can look at all the certificates it manages, and only renew those nearing their expiry via the renew command:

# This will need access to port 443 from the internet, adjust your firewall if needed.\ndocker run --rm -it \\\n-v \"${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/\" \\\n-v \"${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/\" \\\n-p 80:80 \\\n-p 443:443 \\\ncertbot/certbot renew\n

This process can also be automated via cron or systemd timers.

Using a different ACME CA

Certbot does support alternative certificate providers via the --server option. In most cases you'll want to use the default Let's Encrypt.

"},{"location":"config/security/ssl/#example-using-certbot-dns-cloudflare-with-docker","title":"Example using certbot-dns-cloudflare with Docker","text":"

If you are unable get a certificate via the HTTP-01 (port 80) or TLS-ALPN-01 (port 443) challenge types, the DNS-01 challenge can be useful (this challenge can additionally issue wildcard certificates). This guide shows how to use the DNS-01 challenge with Cloudflare as your DNS provider.

Obtain a Cloudflare API token:

  1. Login into your Cloudflare dashboard.
  2. Navigate to the API Tokens page.
  3. Click \"Create Token\", and choose the Edit zone DNS template (Certbot requires the ZONE:DNS:Edit permission).

    Only include the necessary Zone resource configuration

    Be sure to configure \"Zone Resources\" section on this page to Include -> Specific zone -> <your zone here>.

    This restricts the API token to only this zone (domain) which is an important security measure.

  4. Store the API token you received in a file cloudflare.ini with content:

    dns_cloudflare_api_token = YOUR_CLOUDFLARE_TOKEN_HERE\n
    • As this is sensitive data, you should restrict access to it with chmod 600 and chown 0:0.
    • Store the file in a folder if you like, such as docker-data/certbot/secrets/.
  5. Your compose.yaml should include the following:

    services:\nmailserver:\nenvironments:\n# Set SSL certificate type.\n- SSL_TYPE=letsencrypt\nvolumes:\n# Mount the cert folder generated by Certbot:\n- ./docker-data/certbot/certs/:/etc/letsencrypt/:ro\n\ncertbot-cloudflare:\nimage: certbot/dns-cloudflare:latest\ncommand: certonly --dns-cloudflare --dns-cloudflare-credentials /run/secrets/cloudflare-api-token -d mail.example.com\nvolumes:\n- ./docker-data/certbot/certs/:/etc/letsencrypt/\n- ./docker-data/certbot/logs/:/var/log/letsencrypt/\nsecrets:\n- cloudflare-api-token\n\n# Docs: https://docs.docker.com/engine/swarm/secrets/#use-secrets-in-compose\n# WARNING: In compose configs without swarm, the long syntax options have no effect,\n# Ensure that you properly `chmod 600` and `chown 0:0` the file on disk. Effectively treated as a bind mount.\nsecrets:\ncloudflare-api-token:\nfile: ./docker-data/certbot/secrets/cloudflare.ini\n

    Alternative using the docker run command (secrets feature is not available):

    docker run \\\n--volume \"${PWD}/docker-data/certbot/certs/:/etc/letsencrypt/\" \\\n--volume \"${PWD}/docker-data/certbot/logs/:/var/log/letsencrypt/\" \\\n--volume \"${PWD}/docker-data/certbot/secrets/:/tmp/secrets/certbot/\"\ncertbot/dns-cloudflare \\\ncertonly --dns-cloudflare --dns-cloudflare-credentials /tmp/secrets/certbot/cloudflare.ini -d mail.example.com\n
  6. Run the service to provision a certificate:

    docker compose run certbot-cloudflare\n
  7. You should see the following log output:

    Saving debug log to /var/log/letsencrypt/letsencrypt. log | Requesting a certificate for mail.example.com\nWaiting 10 seconds for DNS changes to propagate\nSuccessfully received certificate.\nCertificate is saved at: /etc/letsencrypt/live/mail.example.com/fullchain.pem\nKey is saved at: /etc/letsencrypt/live/mail.example.com/privkey.pem\nThis certificate expires on YYYY-MM-DD.\nThese files will be updated when the certificate renews.\nNEXT STEPS:\n- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal instructions.\n

After completing the steps above, your certificate should be ready to use.

Renewing a certificate (Optional)

We've only demonstrated how to provision a certificate, but it will expire in 90 days and need to be renewed before then.

In the following example, add a new service (certbot-cloudflare-renew) into compose.yaml that will handle certificate renewals:

services:\ncertbot-cloudflare-renew:\nimage: certbot/dns-cloudflare:latest\ncommand: renew --dns-cloudflare --dns-cloudflare-credentials /run/secrets/cloudflare-api-token\nvolumes:\n- ./docker-data/certbot/certs/:/etc/letsencrtypt/\n- ./docker-data/certbot/logs/:/var/log/letsencrypt/\nsecrets:\n- cloudflare-api-token\n

You can manually run this service to renew the cert within 90 days:

docker compose run certbot-cloudflare-renew\n

You should see the following output (The following log was generated with --dry-run options)

Saving debug log to /var/log/letsencrypt/letsencrypt.log\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\nProcessing /etc/letsencrypt/renewal/mail.example.com.conf\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\nAccount registered.\nSimulating renewal of an existing certificate for mail.example.com\nWaiting 10 seconds for DNS changes to propagate\n\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\nCongratulations, all simulated renewals succeeded:\n  /etc/letsencrypt/live/mail.example.com/fullchain.pem (success)\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n

It is recommended to automate this renewal via a task scheduler like a systemd timer or in crontab (crontab example: Checks every day if the certificate should be renewed)

0 0 * * * docker compose -f PATH_TO_YOUR_DOCKER_COMPOSE_YML up certbot-cloudflare-renew\n
"},{"location":"config/security/ssl/#example-using-nginx-proxy-and-acme-companion-with-docker","title":"Example using nginx-proxy and acme-companion with Docker","text":"

If you are running a web server already, port 80 will be in use which Certbot requires. You could use the Certbot --webroot feature, but it is more common to leverage a reverse proxy that manages the provisioning and renewal of certificates for your services automatically.

In the following example, we show how DMS can be run alongside the docker containers nginx-proxy and acme-companion (Referencing: acme-companion documentation):

  1. Start the reverse proxy (nginx-proxy):

    docker run --detach \\\n--name nginx-proxy \\\n--restart always \\\n--publish 80:80 \\\n--publish 443:443 \\\n--volume \"${PWD}/docker-data/nginx-proxy/html/:/usr/share/nginx/html/\" \\\n--volume \"${PWD}/docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/\" \\\n--volume \"${PWD}/docker-data/acme-companion/certs/:/etc/nginx/certs/:ro\" \\\n--volume '/var/run/docker.sock:/tmp/docker.sock:ro' \\\nnginxproxy/nginx-proxy\n
  2. Then start the certificate provisioner (acme-companion), which will provide certificates to nginx-proxy:

    # Inherit `nginx-proxy` volumes via `--volumes-from`, but make `certs/` writeable:\ndocker run --detach \\\n--name nginx-proxy-acme \\\n--restart always \\\n--volumes-from nginx-proxy \\\n--volume \"${PWD}/docker-data/acme-companion/certs/:/etc/nginx/certs/:rw\" \\\n--volume \"${PWD}/docker-data/acme-companion/acme-state/:/etc/acme.sh/\" \\\n--volume '/var/run/docker.sock:/var/run/docker.sock:ro' \\\n--env 'DEFAULT_EMAIL=admin@example.com' \\\nnginxproxy/acme-companion\n
  3. Start the rest of your web server containers as usual.

  4. Start a dummy container to provision certificates for your FQDN (eg: mail.example.com). acme-companion will detect the container and generate a Let's Encrypt certificate for your domain, which can be used by DMS:

    docker run --detach \\\n--name webmail \\\n--env 'VIRTUAL_HOST=mail.example.com' \\\n--env 'LETSENCRYPT_HOST=mail.example.com' \\\n--env 'LETSENCRYPT_EMAIL=admin@example.com' \\\nnginx\n

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

  5. Make sure your mount path to the letsencrypt certificates directory is correct. Edit your compose.yaml for the mailserver service to have volumes added like below:

    volumes:\n- ./docker-data/dms/mail-data/:/var/mail/\n- ./docker-data/dms/mail-state/:/var/mail-state/\n- ./docker-data/dms/config/:/tmp/docker-mailserver/\n- ./docker-data/acme-companion/certs/:/etc/letsencrypt/live/:ro\n
  6. Then from the compose.yaml project directory, run: docker compose up -d mailserver.

"},{"location":"config/security/ssl/#example-using-nginx-proxy-and-acme-companion-with-docker-compose","title":"Example using nginx-proxy and acme-companion with docker-compose","text":"

The following example is the basic setup you need for using nginx-proxy and acme-companion with DMS (Referencing: acme-companion documentation):

Example: compose.yaml

You should have an existing compose.yaml with a mailserver service. Below are the modifications to add for integrating with nginx-proxy and acme-companion services:

services:\n# Add the following `environment` and `volumes` to your existing `mailserver` service:\nmailserver:\nenvironment:\n# SSL_TYPE:         Uses the `letsencrypt` method to find mounted certificates.\n# VIRTUAL_HOST:     The FQDN that `nginx-proxy` will configure itself to handle for HTTP[S] connections.\n# LETSENCRYPT_HOST: The FQDN for a certificate that `acme-companion` will provision and renew.\n- SSL_TYPE=letsencrypt\n- VIRTUAL_HOST=mail.example.com\n- LETSENCRYPT_HOST=mail.example.com\nvolumes:\n- ./docker-data/acme-companion/certs/:/etc/letsencrypt/live/:ro\n\n# If you don't yet have your own `nginx-proxy` and `acme-companion` setup,\n# here is an example you can use:\nreverse-proxy:\nimage: nginxproxy/nginx-proxy\ncontainer_name: nginx-proxy\nrestart: always\nports:\n# Port  80: Required for HTTP-01 challenges to `acme-companion`.\n# Port 443: Only required for containers that need access over HTTPS. TLS-ALPN-01 challenge not supported.\n- \"80:80\"\n- \"443:443\"\nvolumes:\n# `certs/`:      Managed by the `acme-companion` container (_read-only_).\n# `docker.sock`: Required to interact with containers via the Docker API.\n- ./docker-data/nginx-proxy/html/:/usr/share/nginx/html/\n- ./docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/\n- ./docker-data/acme-companion/certs/:/etc/nginx/certs/:ro\n- /var/run/docker.sock:/tmp/docker.sock:ro\n\nacme-companion:\nimage: nginxproxy/acme-companion\ncontainer_name: nginx-proxy-acme\nrestart: always\nenvironment:\n# When `volumes_from: [nginx-proxy]` is not supported,\n# reference the _reverse-proxy_ `container_name` here:\n- NGINX_PROXY_CONTAINER=nginx-proxy\nvolumes:\n# `html/`:       Write ACME HTTP-01 challenge files that `nginx-proxy` will serve.\n# `vhost.d/`:    To enable web access via `nginx-proxy` to HTTP-01 challenge files.\n# `certs/`:      To store certificates and private keys.\n# `acme-state/`: To persist config and state for the ACME provisioner (`acme.sh`).\n# `docker.sock`: Required to interact with containers via the Docker API.\n- ./docker-data/nginx-proxy/html/:/usr/share/nginx/html/\n- ./docker-data/nginx-proxy/vhost.d/:/etc/nginx/vhost.d/\n- ./docker-data/acme-companion/certs/:/etc/nginx/certs/:rw\n- ./docker-data/acme-companion/acme-state/:/etc/acme.sh/\n- /var/run/docker.sock:/var/run/docker.sock:ro\n

Optional ENV vars worth knowing about

Per container ENV that acme-companion will detect to override default provisioning settings:

  • LETSENCRYPT_TEST=true: Recommended during initial setup. Otherwise the default production endpoint has a rate limit of 5 duplicate certificates per week. Overrides ACME_CA_URI to use the Let's Encrypt staging endpoint.
  • LETSENCRYPT_EMAIL: For when you don't use DEFAULT_EMAIL on acme-companion, or want to assign a different email contact for this container.
  • LETSENCRYPT_KEYSIZE: Allows you to configure the type (RSA or ECDSA) and size of the private key for your certificate. Default is RSA 4096.
  • LETSENCRYPT_RESTART_CONTAINER=true: When the certificate is renewed, the entire container will be restarted to ensure the new certificate is used.

acme-companion ENV for default settings that apply to all containers using LETSENCRYPT_HOST:

  • DEFAULT_EMAIL: An email address that the CA (eg: Let's Encrypt) can contact you about expiring certificates, failed renewals, or for account recovery. You may want to use an email address not handled by your mail server to ensure deliverability in the event your mail server breaks.
  • CERTS_UPDATE_INTERVAL: If you need to adjust the frequency to check for renewals. 3600 seconds (1 hour) by default.
  • DEBUG=1: Should be helpful when troubleshooting provisioning issues from acme-companion logs.
  • ACME_CA_URI: Useful in combination with CA_BUNDLE to use a private CA. To change the default Let's Encrypt endpoint to the staging endpoint, use https://acme-staging-v02.api.letsencrypt.org/directory.
  • CA_BUNDLE: If you want to use a private CA instead of Let's Encrypt.

Alternative to required ENV on mailserver service

While you will still need both nginx-proxy and acme-companion containers, you can manage certificates without adding ENV vars to containers. Instead the ENV is moved into a file and uses the acme-companion feature Standalone certificates.

This requires adding another shared volume between nginx-proxy and acme-companion:

services:\nreverse-proxy:\nvolumes:\n- ./docker-data/nginx-proxy/conf.d/:/etc/nginx/conf.d/\n\nacme-companion:\nvolumes:\n- ./docker-data/nginx-proxy/conf.d/:/etc/nginx/conf.d/\n- ./docker-data/acme-companion/standalone.sh:/app/letsencrypt_user_data:ro\n

acme-companion mounts a shell script (standalone.sh), which defines variables to customize certificate provisioning:

# A list IDs for certificates to provision:\nLETSENCRYPT_STANDALONE_CERTS=('mail')\n\n# Each ID inserts itself into the standard `acme-companion` supported container ENV vars below.\n# The LETSENCRYPT_<ID>_HOST var is a list of FQDNs to provision a certificate for as the SAN field:\nLETSENCRYPT_mail_HOST=('mail.example.com')\n\n# Optional variables:\nLETSENCRYPT_mail_TEST=true\nLETSENCRYPT_mail_EMAIL='admin@example.com'\n# RSA-4096 => `4096`, ECDSA-256 => `ec-256`:\nLETSENCRYPT_mail_KEYSIZE=4096\n

Unlike with the equivalent ENV for containers, changes to this file will not be detected automatically. You would need to wait until the next renewal check by acme-companion (every hour by default), restart acme-companion, or manually invoke the service loop:

docker exec nginx-proxy-acme /app/signal_le_service

"},{"location":"config/security/ssl/#example-using-lets-encrypt-certificates-with-a-synology-nas","title":"Example using Let's Encrypt Certificates with 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 compose.yaml declaration file:

volumes:\n- /usr/syno/etc/certificate/_archive/<your-folder>/:/tmp/dms/custom-certs/\nenvironment:\n- SSL_TYPE=manual\n- SSL_CERT_PATH=/tmp/dms/custom-certs/fullchain.pem\n- SSL_KEY_PATH=/tmp/dms/custom-certs/privkey.pem\n

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

"},{"location":"config/security/ssl/#caddy","title":"Caddy","text":"

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:

{\n  debug\n  admin localhost:2019\n  http_port 80\n  https_port 443\n  default_sni example.com\n  key_type rsa4096\n}\n

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

Caddy v2 JSON example snippet
{\n\"apps\": {\n\"http\": {\n\"servers\": {\n\"srv0\": {\n\"listen\": [\n\":443\"\n],\n\"routes\": [\n{\n\"match\": [\n{\n\"host\": [\n\"mail.example.com\",\n]\n}\n],\n\"handle\": [\n{\n\"handler\": \"subroute\",\n\"routes\": [\n{\n\"handle\": [\n{\n\"body\": \"\",\n\"handler\": \"static_response\"\n}\n]\n}\n]\n}\n],\n\"terminal\": true\n},\n]\n}\n}\n},\n\"tls\": {\n\"automation\": {\n\"policies\": [\n{\n\"subjects\": [\n\"mail.example.com\",\n],\n\"key_type\": \"rsa2048\",\n\"issuer\": {\n\"email\": \"admin@example.com\",\n\"module\": \"acme\"\n}\n},\n{\n\"issuer\": {\n\"email\": \"admin@example.com\",\n\"module\": \"acme\"\n}\n}\n]\n}\n}\n}\n}\n

The generated certificates can then be mounted:

volumes:\n- ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.crt:/etc/letsencrypt/live/mail.example.com/fullchain.pem\n- ${CADDY_DATA_DIR}/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/mail.example.com.key:/etc/letsencrypt/live/mail.example.com/privkey.pem\n
"},{"location":"config/security/ssl/#traefik-v2","title":"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 (Postfix and Dovecot).

Wildcard certificates are supported. If your FQDN is mail.example.com and your wildcard certificate is *.example.com, add the ENV: SSL_DOMAIN=example.com.

DMS will select it's certificate from acme.json checking these ENV for a matching FQDN (in order of priority):

  1. ${SSL_DOMAIN}
  2. ${HOSTNAME}
  3. ${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's Encrypt, i.e. Traefik will not issue a certificate without a service / router demanding it.

Example Code

Here is an example setup for docker-compose:

services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\ncontainer_name: mailserver\nhostname: mail.example.com\nvolumes:\n- ./docker-data/traefik/acme.json:/etc/letsencrypt/acme.json:ro\nenvironment:\nSSL_TYPE: letsencrypt\nSSL_DOMAIN: mail.example.com\n# for a wildcard certificate, use\n# SSL_DOMAIN: example.com\n\nreverse-proxy:\nimage: docker.io/traefik:latest #v2.5\ncontainer_name: docker-traefik\nports:\n- \"80:80\"\n- \"443:443\"\ncommand:\n- --providers.docker\n- --entrypoints.http.address=:80\n- --entrypoints.http.http.redirections.entryPoint.to=https\n- --entrypoints.http.http.redirections.entryPoint.scheme=https\n- --entrypoints.https.address=:443\n- --entrypoints.https.http.tls.certResolver=letsencrypt\n- --certificatesresolvers.letsencrypt.acme.email=admin@example.com\n- --certificatesresolvers.letsencrypt.acme.storage=/acme.json\n- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http\nvolumes:\n- ./docker-data/traefik/acme.json:/acme.json\n- /var/run/docker.sock:/var/run/docker.sock:ro\n\nwhoami:\nimage: docker.io/traefik/whoami:latest\nlabels:\n- \"traefik.http.routers.whoami.rule=Host(`mail.example.com`)\"\n
"},{"location":"config/security/ssl/#self-signed-certificates","title":"Self-Signed Certificates","text":"

Warning

Use self-signed certificates only for testing purposes!

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

  • <FQDN>-key.pem
  • <FQDN>-cert.pem
  • demoCA/cacert.pem

Where <FQDN> is the FQDN you've configured for your DMS container.

Add SSL_TYPE=self-signed to your DMS environment variables. Postfix and Dovecot will be configured to use the provided certificate (.pem files above) during container startup.

"},{"location":"config/security/ssl/#generating-a-self-signed-certificate","title":"Generating a self-signed certificate","text":"

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

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

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

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 (eg: docker-data/dms/custom-certs/), then use docker to run that script in a container:

# '--user' is to keep ownership of the files written to\n# the local volume to use your systems User and Group ID values.\ndocker run --rm -it \\\n--user \"$(id -u):$(id -g)\" \\\n--volume \"${PWD}/docker-data/dms/custom-certs/:/tmp/step-ca/\" \\\n--workdir \"/tmp/step-ca/\" \\\n--entrypoint \"/tmp/step-ca/generate-certs.sh\" \\\nsmallstep/step-ca\n
"},{"location":"config/security/ssl/#bring-your-own-certificates","title":"Bring Your Own Certificates","text":"

You can also provide your own certificate files. Add these entries to your compose.yaml:

volumes:\n- ./docker-data/dms/custom-certs/:/tmp/dms/custom-certs/:ro\nenvironment:\n- SSL_TYPE=manual\n# Values should match the file paths inside the container:\n- SSL_CERT_PATH=/tmp/dms/custom-certs/public.crt\n- SSL_KEY_PATH=/tmp/dms/custom-certs/private.key\n

This will mount the path where your certificate files reside locally into the read-only container folder: /tmp/dms/custom-certs.

The local and internal paths may be whatever you prefer, so long as both SSL_CERT_PATH and SSL_KEY_PATH point to the correct internal file paths. The certificate files may also be named to your preference, but should be PEM encoded.

SSL_ALT_CERT_PATH and SSL_ALT_KEY_PATH are additional ENV vars to support a 2nd certificate as a fallback. Commonly known as hybrid or dual certificate support. This is useful for using a modern ECDSA as your primary certificate, and RSA as your fallback for older connections. They work in the same manner as the non-ALT versions.

Info

You may have to restart DMS once the certificates change.

"},{"location":"config/security/ssl/#testing-a-certificate-is-valid","title":"Testing a Certificate is Valid","text":"
  • From your host:

    docker exec mailserver openssl s_client \\\n-connect 0.0.0.0:25 \\\n-starttls smtp \\\n-CApath /etc/ssl/certs/\n
  • Or:

    docker exec mailserver openssl s_client \\\n-connect 0.0.0.0:143 \\\n-starttls imap \\\n-CApath /etc/ssl/certs/\n

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

In addition, to verify certificate dates:

docker exec mailserver openssl s_client \\\n-connect 0.0.0.0:25 \\\n-starttls smtp \\\n-CApath /etc/ssl/certs/ \\\n2>/dev/null | openssl x509 -noout -dates\n
"},{"location":"config/security/ssl/#plain-text-access","title":"Plain-Text Access","text":"

Warning

Not recommended for purposes other than testing.

Add this to docker-data/dms/config/dovecot.cf:

ssl = yes\ndisable_plaintext_auth=no\n

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.
"},{"location":"config/security/ssl/#importing-certificates-obtained-via-another-source","title":"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.

This is a community contributed script, and in most cases you will have better support via our Change Detection service (automatic for SSL_TYPE of manual and letsencrypt) - Unless you're using LDAP which disables the service.

Script Compatibility

  • Relies on private filepaths /etc/dms/tls/cert and /etc/dms/tls/key intended for internal use only.
  • Only supports hard-coded fullchain.key + privkey.pem as your mounted file names. That may not align with your provisioning method.
  • No support for ALT fallback certificates (for supporting dual/hybrid, RSA + ECDSA).

The steps to follow are these:

  1. Transfer the new certificates to ./docker-data/dms/custom-certs/ (volume mounted to: /tmp/ssl/)
  2. You should provide fullchain.key and privkey.pem
  3. Place the script in ./docker-data/dms/config/ (volume mounted to: /tmp/docker-mailserver/)
  4. Make the script executable (chmod +x tomav-renew-certs.sh)
  5. Run the script: docker exec mailserver /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 \\\n-servername mail.example.com \\\n-connect 192.168.0.72:465 \\\n2>/dev/null | openssl x509\n\n# or\n\nopenssl s_client \\\n-servername mail.example.com \\\n-connect mail.example.com:465 \\\n2>/dev/null | openssl x509\n

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

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

What does the script that imports the certificates do:

  1. Check if there are new certs in the internal container folder: /tmp/ssl.
  2. Check with the ssl cert fingerprint if they differ from the current certificates.
  3. If so it will copy the certs to the right places.
  4. 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:

# This script is run inside docker-mailserver via 'docker exec ...', using the 'mail' command to send alerts.\n## code below will alert if certificate expires in less than two weeks\n## please adjust variables!\n## make sure the 'mail -s' command works! Test!\n\nexport SITE_URL=\"mail.example.com\"\nexport SITE_IP_URL=\"192.168.2.72\" # can also use `mail.example.com`\nexport SITE_SSL_PORT=\"993\" # imap port dovecot\n# Below can be from a different domain; like your personal email, not handled by this docker-mailserver:\nexport ALERT_EMAIL_ADDR=\"external-account@gmail.com\"\n\ncertcheck_2weeks=`openssl s_client -connect ${SITE_IP_URL}:${SITE_SSL_PORT} \\\n-servername ${SITE_URL} 2> /dev/null | openssl x509 -noout -checkend 1209600`\n\n####################################\n#notes: output can be\n#Certificate will not expire\n#Certificate will expire\n####################\n\n#echo \"certcheck 2 weeks gives $certcheck_2weeks\"\n\n##automated check you might run by cron or something\n## does the certificate expire within two weeks?\n\nif [ \"$certcheck_2weeks\" = \"Certificate will not expire\" ]; then\necho \"all is well, certwatch 2 weeks says $certcheck_2weeks\"\nelse\necho \"Cert seems to be expiring pretty soon, within two weeks: $certcheck_2weeks\"\necho \"we will send an alert email and log as well\"\nlogger Certwatch: cert $SITE_URL will expire in two weeks\n    echo \"Certwatch: cert $SITE_URL will expire in two weeks\" | mail -s \"cert $SITE_URL expires in two weeks \" $ALERT_EMAIL_ADDR\nfi\n
"},{"location":"config/security/ssl/#custom-dh-parameters","title":"Custom DH Parameters","text":"

By default DMS uses ffdhe4096 from IETF RFC 7919. These are standardized pre-defined DH groups and the only available DH groups for TLS 1.3. It is discouraged to generate your own DH parameters as it is often less secure.

Despite this, if you must use non-standard DH parameters or you would like to swap ffdhe4096 for a different group (eg ffdhe2048); Add your own PEM encoded DH params file via a volume to /tmp/docker-mailserver/dhparams.pem. This will replace DH params for both Dovecot and Postfix services during container startup.

"},{"location":"config/security/understanding-the-ports/","title":"Security | Understanding the Ports","text":""},{"location":"config/security/understanding-the-ports/#quick-reference","title":"Quick Reference","text":"

Prefer ports with Implicit TLS ports, they're more secure than ports using Explicit TLS, and if you use a Reverse Proxy should be less hassle.

"},{"location":"config/security/understanding-the-ports/#overview-of-email-ports","title":"Overview of Email Ports","text":"Protocol Explicit TLS1 Implicit TLS Purpose Enabled by Default ESMTP 25 N/A Transfer2 Yes ESMTP 587 4653 Submission Yes POP3 110 995 Retrieval No IMAP4 143 993 Retrieval Yes
  1. A connection may be secured over TLS when both ends support STARTTLS. On ports 110, 143 and 587, DMS will reject a connection that cannot be secured. Port 25 is required to support insecure connections.
  2. Receives email, DMS 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).
  3. A submission port since 2018 (RFC 8314).
Beware of outdated advice on port 465

There is a common misconception of this port due to it's history detailed by various communities and blogs articles on the topic (including by popular mail relay services).

Port 465 was briefly assigned the role of SMTPS in 1997 as an secure alternative to Port 25 between MTA exchanges. Then RFC 2487 (STARTTLS) while still in a draft status in late 1998 had IANA revoke the SMTPS assignment. The draft history was modified to exclude all mention of port 465 and SMTPS.

In 2018 RFC 8314 was published which revives Port 465 as an Implicit TLS alternative to Port 587 for mail submission. It details very clearly that gaining adoption of 465 as the preferred port will take time. IANA reassigned port 465 as the submissions service. Any unofficial usage as SMTPS is legacy and has been for over two decades.

Understand that port 587 is more broadly supported due to this history and that lots of software in that time has been built or configured with that port in mind. STARTTLS is known to have various CVEs discovered even in recent years, do not be misled by any advice implying it should be preferred over implicit TLS. Trust in more official sources, such as the config Postfix has which acknowledges the submissions port (465).

"},{"location":"config/security/understanding-the-ports/#what-ports-should-i-use-smtp","title":"What Ports Should I Use? (SMTP)","text":"
flowchart LR\n    subgraph your-server [\"Your Server\"]\n        in_25(25) --> server\n        in_465(465) --> server\n        server((\"docker-mailserver<br/>hello@world.com\"))\n        server --- out_25(25)\n        server --- out_465(465)\n    end\n\n    third-party(\"Third-party<br/>(sending you email)\") ---|\"Receive email for<br/>hello@world.com\"| in_25\n\n    subgraph clients [\"Clients (MUA)\"]\n        mua-client(Thunderbird,<br/>Webmail,<br/>Mutt,<br/>etc)\n        mua-service(Backend software<br/>on another server)\n    end\n    clients ---|\"Send email as<br/>hello@world.com\"| in_465\n\n    out_25(25) -->|\"Direct<br/>Delivery\"| tin_25\n    out_465(465) --> relay(\"MTA<br/>Relay Server\") --> tin_25(25)\n\n    subgraph third-party-server[\"Third-party Server\"]\n        third-party-mta(\"MTA<br/>friend@example.com\")\n        tin_25(25) --> third-party-mta\n    end
"},{"location":"config/security/understanding-the-ports/#inbound-traffic-on-the-left","title":"Inbound Traffic (On the left)","text":"

Mail arriving at your server will be processed and stored in a mailbox, or sent outbound to another mail server.

  • Port 25:
    • Think of this like a physical mailbox, anyone can deliver mail to you here. Typically most mail is delivered to you on this port.
    • DMS will actively filter email delivered on this port for spam or viruses, and refuse mail from known bad sources.
    • Connections to this port may be secure through STARTTLS, but is not mandatory as mail is allowed to arrive via an unencrypted connection.
    • It is possible for internal clients to submit mail to be sent outbound (without requiring authentication), but that is discouraged. Prefer the submission ports.
  • Port 465 and 587:
    • This is the equivalent of a post office box where you would send email to be delivered on your behalf (DMS is that metaphorical post office, aka the MTA).
    • These two ports are known as the submission ports, they enable mail to be sent outbound to another MTA (eg: Outlook or Gmail) but require authentication via a mail account.
    • For inbound traffic, this is relevant when you send mail from your MUA (eg: ThunderBird). It's also used when DMS is configured as a mail relay, or when you have a service sending transactional mail (eg: order confirmations, password resets, notifications) through DMS.
    • Prefer port 465 over port 587, as 465 provides Implicit TLS.

Note

When submitting mail (inbound) to be sent (outbound), this involves two separate connections to negotiate and secure. There may be additional intermediary connections which DMS is not involved in, and thus unable to ensure encrypted transit throughout delivery.

"},{"location":"config/security/understanding-the-ports/#outbound-traffic-on-the-right","title":"Outbound Traffic (On the Right)","text":"

Mail being sent from your server is either being relayed through another MTA (eg: SendGrid), or direct to an MTA responsible for an email address (eg: Gmail).

  • Port 25:
    • As most MTA use port 25 to receive inbound mail, when no authenticated relay is involved this is the outbound port used.
    • Outbound traffic on this port is often blocked by service providers (eg: VPS, ISP) to prevent abuse by spammers. If the port cannot be unblocked, you will need to relay outbound mail through a service to send on your behalf.
  • Port 465 and 587:
    • Submission ports for outbound traffic establish trust to forward mail through a third-party relay service. This requires authenticating to an account on the relay service. The relay will then deliver the mail through port 25 on your behalf.
    • These are the two typical ports used, but smart hosts like SendGrid often document support for additional non-standard ports as alternatives if necessary.
    • Usually you'll only use these outbound ports for relaying. It is possible to deliver directly to the relevant MTA for email address, but requires having credentials for each MTA.

Tip

DMS can function as a relay too, but professional relay services have a trusted reputation (which increases success of delivery).

An MTA with low reputation can affect if mail is treated as junk, or even rejected.

Note

At best, you can only ensure a secure connection between the MTA you directly connect to. The receiving MTA may relay that mail to another MTA (and so forth), each connection may not be enforcing TLS.

"},{"location":"config/security/understanding-the-ports/#explicit-vs-implicit-tls","title":"Explicit vs Implicit TLS","text":""},{"location":"config/security/understanding-the-ports/#explicit-tls-aka-opportunistic-tls-opt-in-encryption","title":"Explicit TLS (aka Opportunistic TLS) - Opt-in Encryption","text":"

Communication on these ports begin in cleartext. Upgrading to an encrypted connection must be requested explicitly through the STARTTLS protocol and successfully negotiated.

Sometimes a reverse-proxy is involved, but is misconfigured or lacks support for the STARTTLS negotiation to succeed.

Note

  • By default, DMS is configured to reject connections that fail to establish a secure connection (when authentication is required), rather than allow an insecure connection.
  • Port 25 does not require authentication. If STARTTLS is unsuccessful, mail can be received over an unencrypted connection. You can better secure this port between trusted parties with the addition of MTA-STS, STARTTLS Policy List, DNSSEC and DANE.

Warning

STARTTLS continues to have vulnerabilities found (Nov 2021 article), as per RFC 8314 (Section 4.1) you are encouraged to prefer Implicit TLS where possible.

Support for STARTTLS is not always implemented correctly, which can lead to leaking credentials (like a 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.

"},{"location":"config/security/understanding-the-ports/#implicit-tls-enforced-encryption","title":"Implicit TLS - Enforced Encryption","text":"

Communication on these ports are always encrypted (enforced, thus implicit), avoiding the potential risks with STARTTLS (Explicit TLS).

While Explicit TLS can provide the same benefit (when STARTTLS is successfully negotiated), Implicit TLS more reliably avoids concerns with connection manipulation and compatibility.

"},{"location":"config/security/understanding-the-ports/#security","title":"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.

"},{"location":"config/security/understanding-the-ports/#tls-connections-for-a-mail-server-compared-to-web-browsers","title":"TLS connections for a Mail Server, 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 does not provide the equivalent safety that HTTPS does 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.

"},{"location":"contributing/general/","title":"Contributing | General Information","text":""},{"location":"contributing/general/#coding-style","title":"Coding Style","text":"

When refactoring, writing or altering scripts or other files, 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. 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.
  3. Use the provided .editorconfig file.
  4. Use /bin/bash instead of /bin/sh in scripts
"},{"location":"contributing/general/#documentation","title":"Documentation","text":"

Make sure to select edge in the dropdown menu at the top. Navigate to the page you would like to edit and click the edit button in the top right. This allows you to make changes and create a pull-request.

Alternatively you can make the changes locally. For that you'll need to have Docker installed. Navigate into the docs/ directory. Then run:

docker run --rm -it -p 8000:8000 -v \"${PWD}:/docs\" squidfunk/mkdocs-material\n

This serves the documentation on your local machine on port 8000. Each change will be hot-reloaded onto the page you view, just edit, save and look at the result.

"},{"location":"contributing/issues-and-pull-requests/","title":"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.

"},{"location":"contributing/issues-and-pull-requests/#opening-an-issue","title":"Opening an Issue","text":"

Attention

Before opening an issue, read the README carefully, study the docs for your version (maybe latest), 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 DMS with the environment variable LOG_LEVEL set to debug or trace 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.

"},{"location":"contributing/issues-and-pull-requests/#filing-a-bug-report","title":"Filing a Bug Report","text":"

Thank you for participating in this project and reporting a bug. Docker Mail Server (DMS) is a community-driven project, and each contribution counts!

Maintainers and moderators are volunteers. We greatly appreciate reports that take the time to provide detailed information via the template, enabling us to help you in the best and quickest way. Ignoring the template provided may seem easier, but discourages receiving any support (via assignment of the label meta/no template - no support).

Markdown formatting can be used in almost all text fields (unless stated otherwise in the description).

Be as precise as possible, and if in doubt, it's best to add more information that too few.

When an option is marked with \"not officially supported\" / \"unsupported\", then support is dependent on availability from specific maintainers.

"},{"location":"contributing/issues-and-pull-requests/#pull-requests","title":"Pull Requests","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.

"},{"location":"contributing/issues-and-pull-requests/#submit-a-pull-request","title":"Submit a Pull-Request","text":"

The development workflow is the following:

  1. Fork the project and clone your fork with git clone --recurse-submodules ... or run git submodule update --init --recursive after you cloned your fork
  2. Write the code that is needed :D
  3. Add integration tests if necessary
  4. Prepare your environment and run linting and tests
  5. Document your improvements if necessary (e.g. if you introduced new environment variables, describe those in the ENV documentation) and add your changes the changelog under the \"Unreleased\" section
  6. 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.

"},{"location":"contributing/tests/","title":"Tests","text":"

Program testing can be used to show the presence of bugs, but never to show their absence!

\u2013 Edsger Wybe Dijkstra

"},{"location":"contributing/tests/#introduction","title":"Introduction","text":"

DMS employs a variety of unit and integration tests. All tests and associated configuration is stored in the test/ directory. If you want to change existing functionality or integrate a new feature into DMS, you will probably need to work with our test suite.

Can I use macOS?

We do not support running linting, tests, etc. on macOS at this time. Please use a Linux VM, Debian/Ubuntu is recommended.

"},{"location":"contributing/tests/#about","title":"About","text":"

We use BATS (Bash Automated Testing System) and additional support libraries. BATS is very similar to Bash, and one can easily and quickly get an understanding of how tests in a single file are run. A test file template provides a minimal working example for newcomers to look at.

"},{"location":"contributing/tests/#structure","title":"Structure","text":"

The test/ directory contains multiple directories. Among them is the bats/ directory (which is the BATS git submodule) and the helper/ directory. The latter is especially interesting because it contains common support functionality used in almost every test. Actual tests are located in test/tests/.

Test suite undergoing refactoring

We are currently in the process of restructuring all of our tests. Tests will be moved into test/tests/parallel/ and new tests should be placed there as well.

"},{"location":"contributing/tests/#using-our-helper-functions","title":"Using Our Helper Functions","text":"

There are many functions that aid in writing tests. We urge you to use them! They will not only ease writing a test but they will also do their best to ensure there are no race conditions or other unwanted side effects. To learn about the functions we provide, you can:

  1. look into existing tests for helper functions we already used
  2. look into the test/helper/ directory which contains all files that can (and will) be loaded in test files

We encourage you to try both of the approaches mentioned above. To make understanding and using the helper functions easy, every function contains detailed documentation comments. Read them carefully!

"},{"location":"contributing/tests/#how-are-tests-run","title":"How Are Tests Run?","text":"

Tests are split into two categories:

  • test/tests/parallel/: Multiple test files are run concurrently to reduce the required time to complete the test suite. A test file will presently run it's own defined test-cases in a sequential order.
  • test/tests/serial/: Each test file is queued up to run sequentially. Tests that are unable to support running concurrently belong here.

Parallel tests are further partitioned into smaller sets. If your system has the resources to support running more than one of those sets at a time this is perfectly ok (our CI runs tests by distributing the sets across multiple test runners). Care must be taken not to mix running the serial tests while a parallel set is also running; this is handled for you when using make tests.

"},{"location":"contributing/tests/#running-tests","title":"Running Tests","text":""},{"location":"contributing/tests/#prerequisites","title":"Prerequisites","text":"

To run the test suite, you will need to:

  1. Install Docker
  2. Install jq and (GNU) parallel (under Ubuntu, use sudo apt-get -y install jq parallel)
  3. Execute git submodule update --init --recursive if you haven't already initialized the git submodules
"},{"location":"contributing/tests/#executing-tests","title":"Executing Test(s)","text":"

We use make to run commands.

  1. Run make build to create or update the local mailserver-testing:ci Docker image (using the projects Dockerfile)
  2. Run all tests: make clean tests
  3. Run a single test: make clean generate-accounts test/<TEST NAME WITHOUT .bats SUFFIX>
  4. Run multiple unrelated tests: make clean generate-accounts test/<TEST NAME WITHOUT .bats SUFFIX>,<TEST NAME WITHOUT .bats SUFFIX> (just add a , and then immediately write the new test name)
  5. Run a whole set or all serial tests: make clean generate-accounts tests/parallel/setX where X is the number of the set or make clean generate-accounts tests/serial
Setting the Degree of Parallelization for Tests

If your machine is capable, you can increase the amount of tests that are run simultaneously by prepending the make clean all command with BATS_PARALLEL_JOBS=X (i.e. BATS_PARALLEL_JOBS=X make clean all). This wil speed up the test procedure. You can also run all tests in serial by setting BATS_PARALLEL_JOBS=1 this way.

The default value of BATS_PARALLEL_JOBS is 2. This can be increased if your system has the resources spare to support running more jobs at once to complete the test suite sooner.

Test Output when Running in Parallel

When running tests in parallel (with make clean generate-accounts tests/parallel/setX), BATS will delay outputting the results until completing all test cases within a file.

This likewise delays the reporting of test-case failures. When troubleshooting parallel set tests, you may prefer to run specific tests you're working on serially (as demonstrated in the example below).

When writing tests, ensure that parallel set tests still pass when run in parallel. You need to account for other tests running in parallel that may interfere with your own tests logic.

Tip

You may use make run-local-instance to run a version of the image built locally to test and edit your changes in a running DMS instance.

"},{"location":"contributing/tests/#an-example","title":"An Example","text":"

In this example, you've made a change to the Rspamd feature support (or adjusted it's tests). First verify no regressions have been introduced by running it's specific test file:

$ make clean generate-accounts test/rspamd\nrspamd.bats\n  \u2713 [Rspamd] Postfix's main.cf was adjusted [12]\n  \u2713 [Rspamd] normal mail passes fine [44]\n  \u2713 [Rspamd] detects and rejects spam [122]\n  \u2713 [Rspamd] detects and rejects virus [189]\n

As your feature work progresses your change for Rspamd also affects ClamAV. As your change now spans more than just the Rspamd test file, you could run multiple test files serially:

$ make clean generate-accounts test/rspamd,clamav\nrspamd.bats\n  \u2713 [Rspamd] Postfix's main.cf was adjusted [12]\n  \u2713 [Rspamd] normal mail passes fine [44]\n  \u2713 [Rspamd] detects and rejects spam [122]\n  \u2713 [Rspamd] detects and rejects virus [189]\n\nclamav.bats\n  \u2713 [ClamAV] log files exist at /var/log/mail directory [68]\n  \u2713 [ClamAV] should be identified by Amavis [67]\n  \u2713 [ClamAV] freshclam cron is enabled [76]\n  \u2713 [ClamAV] env CLAMAV_MESSAGE_SIZE_LIMIT is set correctly [63]\n  \u2713 [ClamAV] rejects virus [60]\n

You're almost finished with your change before submitting it as a PR. It's a good idea to run the full parallel set those individual tests belong to (especially if you've modified any tests):

$ make clean generate-accounts tests/parallel/set1\ndefault_relay_host.bats\n  \u2713 [Relay] (ENV) 'DEFAULT_RELAY_HOST' should configure 'main.cf:relayhost' [88]\n\nspam_virus/amavis.bats\n  \u2713 [Amavis] SpamAssassin integration should be active [1165]\n\nspam_virus/clamav.bats\n  \u2713 [ClamAV] log files exist at /var/log/mail directory [73]\n  \u2713 [ClamAV] should be identified by Amavis [67]\n  \u2713 [ClamAV] freshclam cron is enabled [76]\n...\n

Even better, before opening a PR run the full test suite:

$ make clean tests\n
"},{"location":"examples/tutorials/basic-installation/","title":"Tutorials | Basic Installation","text":""},{"location":"examples/tutorials/basic-installation/#a-basic-example-with-relevant-environmental-variables","title":"A Basic Example With Relevant Environmental Variables","text":"

This example provides you only with a basic example of what a minimal setup could look like. We strongly recommend that you go through the configuration file yourself and adjust everything to your needs. The default compose.yaml can be used for the purpose out-of-the-box, see the Usage chapter.

services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\ncontainer_name: mailserver\n# Provide the FQDN of your mail server here (Your DNS MX record should point to this value)\nhostname: mail.example.com\nports:\n- \"25:25\"\n- \"465:465\"\n- \"587:587\"\n- \"993:993\"\nvolumes:\n- ./docker-data/dms/mail-data/:/var/mail/\n- ./docker-data/dms/mail-state/:/var/mail-state/\n- ./docker-data/dms/mail-logs/:/var/log/mail/\n- ./docker-data/dms/config/:/tmp/docker-mailserver/\n- /etc/localtime:/etc/localtime:ro\nenvironment:\n- ENABLE_RSPAMD=1\n- ENABLE_CLAMAV=1\n- ENABLE_FAIL2BAN=1\ncap_add:\n- NET_ADMIN # For Fail2Ban to work\nrestart: always\n
"},{"location":"examples/tutorials/basic-installation/#a-basic-ldap-setup","title":"A Basic LDAP Setup","text":"

Note There are currently no LDAP maintainers. If you encounter issues, please raise them in the issue tracker, but be aware that the core maintainers team will most likely not be able to help you. We would appreciate and we encourage everyone to actively participate in maintaining LDAP-related code by becoming a maintainer!

services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\ncontainer_name: mailserver\n# Provide the FQDN of your mail server here (Your DNS MX record should point to this value)\nhostname: mail.example.com\nports:\n- \"25:25\"\n- \"465:465\"\n- \"587:587\"\n- \"993:993\"\nvolumes:\n- ./docker-data/dms/mail-data/:/var/mail/\n- ./docker-data/dms/mail-state/:/var/mail-state/\n- ./docker-data/dms/mail-logs/:/var/log/mail/\n- ./docker-data/dms/config/:/tmp/docker-mailserver/\n- /etc/localtime:/etc/localtime:ro\nenvironment:\n- ACCOUNT_PROVISIONER=LDAP\n- LDAP_SERVER_HOST=ldap # your ldap container/IP/ServerName\n- LDAP_SEARCH_BASE=ou=people,dc=localhost,dc=localdomain\n- LDAP_BIND_DN=cn=admin,dc=localhost,dc=localdomain\n- LDAP_BIND_PW=admin\n- LDAP_QUERY_FILTER_USER=(&(mail=%s)(mailEnabled=TRUE))\n- LDAP_QUERY_FILTER_GROUP=(&(mailGroupMember=%s)(mailEnabled=TRUE))\n- LDAP_QUERY_FILTER_ALIAS=(|(&(mailAlias=%s)(objectClass=PostfixBookMailForward))(&(mailAlias=%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)))\n- LDAP_QUERY_FILTER_DOMAIN=(|(&(mail=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailGroupMember=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailalias=*@%s)(objectClass=PostfixBookMailForward)))\n- DOVECOT_PASS_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))\n- DOVECOT_USER_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))\n- ENABLE_SASLAUTHD=1\n- SASLAUTHD_MECHANISMS=ldap\n- SASLAUTHD_LDAP_SERVER=ldap\n- SASLAUTHD_LDAP_BIND_DN=cn=admin,dc=localhost,dc=localdomain\n- SASLAUTHD_LDAP_PASSWORD=admin\n- SASLAUTHD_LDAP_SEARCH_BASE=ou=people,dc=localhost,dc=localdomain\n- SASLAUTHD_LDAP_FILTER=(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%U))\n- POSTMASTER_ADDRESS=postmaster@localhost.localdomain\nrestart: always\n
"},{"location":"examples/tutorials/basic-installation/#using-dms-as-a-local-mail-relay-for-containers","title":"Using DMS as a local mail relay for containers","text":"

Info

This was originally a community contributed guide. Please let us know via a Github Issue if you're having any difficulty following the guide so that we can update it.

This guide is focused on only using SMTP ports (not POP3 and IMAP) with the intent to relay mail received from another service to an external email address (eg: user@gmail.com). It is not intended for mailbox storage of real users.

In this setup DMS is not intended to receive email from the outside world, so no anti-spam or anti-virus software is needed, making the service lighter to run.

setup

The setup command used below is to be run inside the container.

Open Relays

Adding the docker network's gateway to the list of trusted hosts (eg: 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.

  1. Create the file compose.yaml with a content like this:

    Example

    services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\ncontainer_name: mailserver\n# Provide the FQDN of your mail server here (Your DNS MX record should point to this value)\nhostname: mail.example.com\nports:\n- \"25:25\"\n- \"587:587\"\n- \"465:465\"\nvolumes:\n- ./docker-data/dms/mail-data/:/var/mail/\n- ./docker-data/dms/mail-state/:/var/mail-state/\n- ./docker-data/dms/mail-logs/:/var/log/mail/\n- ./docker-data/dms/config/:/tmp/docker-mailserver/\n- /etc/localtime:/etc/localtime:ro\nenvironment:\n- ENABLE_FAIL2BAN=1\n# Using letsencrypt for SSL/TLS certificates:\n- SSL_TYPE=letsencrypt\n# Allow sending emails from other docker containers:\n# Beware creating an Open Relay: https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#permit_docker\n- PERMIT_DOCKER=network\n# You may want to enable this: https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/#spoof_protection\n# See step 6 below, which demonstrates setup with enabled/disabled SPOOF_PROTECTION:\n- SPOOF_PROTECTION=0\ncap_add:\n- NET_ADMIN # For Fail2Ban to work\nrestart: always\n

    The docs have a detailed page on Environment Variables for reference.

    Firewalled ports

    If you have a firewall running, you may need to open ports 25, 587 and 465.

    For example, with the firewall ufw, run:

    ufw allow 25\nufw allow 587\nufw allow 465\n

    Caution: This may not be sound advice.

  2. Configure your DNS service to use an MX record for the hostname (eg: mail) you configured in the previous step and add the SPF TXT record.

    If you manually manage the DNS zone file for the domain

    It would look something like this:

    $ORIGIN example.com\n@     IN  A      10.11.12.13\nmail  IN  A      10.11.12.13\n\n; mail server for example.com\n@     IN  MX  10 mail.example.com.\n\n; Add SPF record\n@     IN  TXT    \"v=spf1 mx -all\"\n

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

  3. Generate DKIM keys for your domain via setup config dkim.

    Copy the content of the file docker-data/dms/config/opendkim/keys/example.com/mail.txt and add it to your DNS records as a TXT like SPF was handled above.

    I use bind9 for managing my domains, so I just paste it on example.com.db:

    mail._domainkey IN      TXT     ( \"v=DKIM1; h=sha256; k=rsa; \"\n        \"p=MIIBIjANBgkqhkiG9w0BAQEFACAQ8AMIIBCgKCAQEAaH5KuPYPSF3Ppkt466BDMAFGOA4mgqn4oPjZ5BbFlYA9l5jU3bgzRj3l6/Q1n5a9lQs5fNZ7A/HtY0aMvs3nGE4oi+LTejt1jblMhV/OfJyRCunQBIGp0s8G9kIUBzyKJpDayk2+KJSJt/lxL9Iiy0DE5hIv62ZPP6AaTdHBAsJosLFeAzuLFHQ6USyQRojefqFQtgYqWQ2JiZQ3\"\n        \"iqq3bD/BVlwKRp5gH6TEYEmx8EBJUuDxrJhkWRUk2VDl1fqhVBy8A9O7Ah+85nMrlOHIFsTaYo9o6+cDJ6t1i6G1gu+bZD0d3/3bqGLPBQV9LyEL1Rona5V7TJBGg099NQkTz1IwIDAQAB\" )  ; ----- DKIM key mail for example.com\n
  4. Get an SSL certificate, we have a guide for you here (Let's Encrypt is a popular service to get free SSL certificates).

  5. Start DMS and check the terminal output for any errors: docker compose up.

  6. Create email accounts and aliases:

    With SPOOF_PROTECTION=0

    setup email add admin@example.com passwd123\nsetup email add info@example.com passwd123\nsetup alias add admin@example.com external-account@gmail.com\nsetup alias add info@example.com external-account@gmail.com\nsetup email list\nsetup alias list\n

    Aliases make sure that any email that comes to these accounts is forwarded to your third-party email address (external-account@gmail.com), where they are retrieved (eg: via third-party web or mobile app), instead of connecting directly to docker-mailserer with POP3 / IMAP.

    With SPOOF_PROTECTION=1

    setup email add admin.gmail@example.com passwd123\nsetup email add info.gmail@example.com passwd123\nsetup alias add admin@example.com admin.gmail@example.com\nsetup alias add info@example.com info.gmail@example.com\nsetup alias add admin.gmail@example.com external-account@gmail.com\nsetup alias add info.gmail@example.com external-account@gmail.com\nsetup email list\nsetup alias list\n

    This extra step is required to avoid the 553 5.7.1 Sender address rejected: not owned by user error (the accounts used for submitting mail to Gmail are admin.gmail@example.com and info.gmail@example.com)

  7. Send some test emails to these addresses and make other tests. Once everything is working well, stop the container with ctrl+c and start it again as a daemon: docker compose up -d.

"},{"location":"examples/tutorials/blog-posts/","title":"Tutorials | Blog Posts","text":"

This site lists blog entries that write about the project. If you blogged about DMS let us know so we can add it here!

  • Installing docker-mailserver by @andrewlow
  • Self hosted mail-server by @matrixes
  • Docker-mailserver on kubernetes by @ErikEngerd
"},{"location":"examples/tutorials/crowdsec/","title":"Tutorials | Crowdsec","text":"

What is Crowdsec?

Crowdsec is an open source software that detects and blocks attackers using log analysis. It has access to a global community-wide IP reputation database.

Source

"},{"location":"examples/tutorials/crowdsec/#installation","title":"Installation","text":"

Crowdsec supports multiple installation methods, however this page will use the docker installation.

"},{"location":"examples/tutorials/crowdsec/#docker-mailserver","title":"Docker mailserver","text":"

In your compose.yaml for the DMS service, add a bind mount volume for /var/log/mail. This is to share the DMS logs to a separate crowdsec container.

Example

services:\nmailserver:\n- /docker-data/dms/mail-logs/:/var/log/mail/\n
"},{"location":"examples/tutorials/crowdsec/#crowdsec","title":"Crowdsec","text":"

The crowdsec container should also bind mount the same host path for the DMS logs that was added in the DMS example above.

services:\nimage: crowdsecurity/crowdsec\nrestart: unless-stopped\nports:\n- \"8080:8080\"\n- \"6060:6060\"\nvolumes:\n- /docker-data/dms/mail-logs/:/var/log/dms:ro\n- ./acquis.d:/etc/crowdsec/acquis.d\n- crowdsec-db:/var/lib/crowdsec/data/\nenvironment:\n# These collection contains parsers and scenarios for postfix and dovecot\nCOLLECTIONS: crowdsecurity/postfix crowdsecurity/dovecot\nTZ: Europe/Paris\nvolumes:\ncrowdsec-db:\n
"},{"location":"examples/tutorials/crowdsec/#configuration","title":"Configuration","text":"

Configure crowdsec to read and parse DMS logs file.

Example

Create the file dms.yml in ./acquis.d/

---\nsource: file\nfilenames:\n- /var/log/dms/mail.log\nlabels:\ntype: syslog\n

Warning

Crowdsec on its own is just a detection software, the remediation is done by components called bouncers. This page does not explain how to install or configure a bouncer. It can be found in crowdsec documentation.

"},{"location":"examples/tutorials/docker-build/","title":"Tutorials | Docker Build","text":""},{"location":"examples/tutorials/docker-build/#building-your-own-docker-image","title":"Building your own Docker image","text":""},{"location":"examples/tutorials/docker-build/#submodules","title":"Submodules","text":"

You'll need to retrieve the git submodules prior to building your own Docker image. From within your copy of the git repo run the following to retrieve the submodules and build the Docker image:

git submodule update --init --recursive\ndocker build --tag <YOUR CUSTOM IMAGE NAME> .\n

Or, you can clone and retrieve the submodules in one command:

git clone --recurse-submodules https://github.com/docker-mailserver/docker-mailserver\n
"},{"location":"examples/tutorials/docker-build/#about-docker","title":"About Docker","text":""},{"location":"examples/tutorials/docker-build/#minimum-supported-version","title":"Minimum supported version","text":"

We make use of build features that require a recent version of Docker. v23.0 or newer is advised, but earlier releases may work.

  • To get the latest version for your distribution, please have a look at the official installation documentation for Docker.
  • If you are using a version of Docker prior to v23.0, you will need to enable BuildKit via the ENV DOCKER_BUILDKIT=1.
"},{"location":"examples/tutorials/docker-build/#build-arguments-optional","title":"Build Arguments (Optional)","text":"

The Dockerfile includes several build ARG instructions that can be configured:

  • DOVECOT_COMMUNITY_REPO: Install Dovecot from the community repo instead of from Debian (default = 1)
  • DMS_RELEASE: The image version (default = edge)
  • VCS_REVISION: The git commit hash used for the build (default = unknown)

Note

  • DMS_RELEASE (when not edge) will be used to check for updates from our GH releases page at runtime due to the default feature ENABLE_UPDATE_CHECK=1.
  • Both DMS_RELEASE and VCS_REVISION are also used with opencontainers metadata LABEL instructions.
"},{"location":"examples/tutorials/mailserver-behind-proxy/","title":"Tutorials | Mail Server behind a Proxy","text":""},{"location":"examples/tutorials/mailserver-behind-proxy/#using-dms-behind-a-proxy","title":"Using DMS behind a Proxy","text":""},{"location":"examples/tutorials/mailserver-behind-proxy/#information","title":"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.

"},{"location":"examples/tutorials/mailserver-behind-proxy/#configuration-of-the-used-proxy-software","title":"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 achieved the same goal using different proxy software below:

Traefik v2

Truncated configuration of traefik itself:

services:\nreverse-proxy:\nimage: docker.io/traefik:latest # v2.5\ncontainer_name: docker-traefik\nrestart: always\ncommand:\n- \"--providers.docker\"\n- \"--providers.docker.exposedbydefault=false\"\n- \"--providers.docker.network=proxy\"\n- \"--entrypoints.web.address=:80\"\n- \"--entryPoints.websecure.address=:443\"\n- \"--entryPoints.smtp.address=:25\"\n- \"--entryPoints.smtp-ssl.address=:465\"\n- \"--entryPoints.imap-ssl.address=:993\"\n- \"--entryPoints.sieve.address=:4190\"\nports:\n- \"25:25\"\n- \"465:465\"\n- \"993:993\"\n- \"4190:4190\"\n[...]\n

Truncated list of necessary labels on the DMS container:

services:\nmailserver:\nimage: ghcr.io/docker-mailserver/docker-mailserver:latest\ncontainer_name: mailserver\nhostname: mail.example.com\nrestart: always\nnetworks:\n- proxy\nlabels:\n- \"traefik.enable=true\"\n- \"traefik.tcp.routers.smtp.rule=HostSNI(`*`)\"\n- \"traefik.tcp.routers.smtp.entrypoints=smtp\"\n- \"traefik.tcp.routers.smtp.service=smtp\"\n- \"traefik.tcp.services.smtp.loadbalancer.server.port=25\"\n- \"traefik.tcp.services.smtp.loadbalancer.proxyProtocol.version=1\"\n- \"traefik.tcp.routers.smtp-ssl.rule=HostSNI(`*`)\"\n- \"traefik.tcp.routers.smtp-ssl.entrypoints=smtp-ssl\"\n- \"traefik.tcp.routers.smtp-ssl.tls.passthrough=true\"\n- \"traefik.tcp.routers.smtp-ssl.service=smtp-ssl\"\n- \"traefik.tcp.services.smtp-ssl.loadbalancer.server.port=465\"\n- \"traefik.tcp.services.smtp-ssl.loadbalancer.proxyProtocol.version=1\"\n- \"traefik.tcp.routers.imap-ssl.rule=HostSNI(`*`)\"\n- \"traefik.tcp.routers.imap-ssl.entrypoints=imap-ssl\"\n- \"traefik.tcp.routers.imap-ssl.service=imap-ssl\"\n- \"traefik.tcp.routers.imap-ssl.tls.passthrough=true\"\n- \"traefik.tcp.services.imap-ssl.loadbalancer.server.port=10993\"\n- \"traefik.tcp.services.imap-ssl.loadbalancer.proxyProtocol.version=2\"\n- \"traefik.tcp.routers.sieve.rule=HostSNI(`*`)\"\n- \"traefik.tcp.routers.sieve.entrypoints=sieve\"\n- \"traefik.tcp.routers.sieve.service=sieve\"\n- \"traefik.tcp.services.sieve.loadbalancer.server.port=4190\"\n[...]\n

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

"},{"location":"examples/tutorials/mailserver-behind-proxy/#configuration-of-the-backend-dovecot-and-postfix","title":"Configuration of the Backend (dovecot and postfix)","text":"

The following changes can be achieved 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 docker-data/dms/config/postfix-main.cf:

postscreen_upstream_proxy_protocol = haproxy\n

and to docker-data/dms/config/postfix-master.cf:

submission/inet/smtpd_upstream_proxy_protocol=haproxy\nsubmissions/inet/smtpd_upstream_proxy_protocol=haproxy\n

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

haproxy_trusted_networks = <your-proxy-ip>, <optional-cidr-notation>\nhaproxy_timeout = 3 secs\nservice imap-login {\ninet_listener imaps {\nhaproxy = yes\nssl = yes\nport = 10993\n}\n}\n

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.

"},{"location":"examples/use-cases/auth-lua/","title":"Examples | Use Cases | Lua Authentication","text":""},{"location":"examples/use-cases/auth-lua/#introduction","title":"Introduction","text":"

Dovecot has the ability to let users create their own custom user provisioning and authentication providers in Lua. This allows any data source that can be approached from Lua to be used for authentication, including web servers. It is possible to do more with Dovecot and Lua, but other use cases fall outside of the scope of this documentation page.

Community contributed guide

Dovecot authentication via Lua scripting is not officially supported in DMS. No assistance will be provided should you encounter any issues.

DMS provides the required packages to support this guide. Note that these packages will be removed should they introduce any future maintenance burden.

The example in this guide relies on the current way in which DMS works with Dovecot configuration files. Changes to this to accommodate new authentication methods such as OpenID Connect will likely break this example in the future. This guide is updated on a best-effort base.

Dovecot's Lua support can be used for user provisioning (userdb functionality) and/or password verification (passdb functionality). Consider using other userdb and passdb options before considering Lua, since Lua does require the use of additional (unsupported) program code that might require maintenance when updating DMS.

Each implementation of Lua-based authentication is custom. Therefore it is impossible to write documentation that covers every scenario. Instead, this page describes a single example scenario. If that scenario is followed, you will learn vital aspects that are necessary to kickstart your own Lua development:

  • How to override Dovecot's default configuration to disable parts that conflict with your scenario.
  • How to make Dovecot use your Lua script.
  • How to add your own Lua script and any libraries it uses.
  • How to debug your Lua script.
"},{"location":"examples/use-cases/auth-lua/#the-example-scenario","title":"The example scenario","text":"

This scenario starts with DMS being configured to use LDAP for mailbox identification, user authorization and user authentication. In this scenario, Nextcloud is also a service that uses the same LDAP server for user identification, authorization and authentication.

The goal of this scenario is to have Dovecot not authenticate the user against LDAP, but against Nextcloud using an application password. The idea behind this is that a compromised mailbox password does not compromise the user's account entirely. To make this work, Nextcloud is configured to deny the use of account passwords by clients and to disable account password reset through mail verification.

If the application password is configured correctly, an adversary can only use it to access the user's mailbox on DMS, and CalDAV and CardDAV data on Nextcloud. File access through WebDAV can be disabled for the application password used to access mail. Having CalDAV and CardDAV compromised by the same password is a minor setback. If an adversary gets access to a Nextcloud application password through a device of the user, it is likely that the adversary also gets access to the user's calendars and contact lists anyway (locally or through the same account settings used for mail and CalDAV/CardDAV synchronization). The user's stored files in Nextcloud, the LDAP account password and any other services that rely on it would still be protected. A bonus is that a user is able to revoke and renew the mailbox password in Nextcloud for whatever reason, through a friendly user interface with all the security measures with which the Nextcloud instance is configured (e.g. verification of the current account password).

A drawback of this method is that any (compromised) Nextcloud application password can be used to access the user's mailbox. This introduces a risk that a Nextcloud application password used for something else (e.g. WebDAV file access) is compromised and used to access the user's mailbox. Discussion of that risk and possible mitigations fall outside of the scope of this scenario.

To answer the questions asked earlier for this specific scenario:

  1. Do I want to use Lua to identify mailboxes and verify that users are are authorized to use mail services? No. Provisioning is done through LDAP.
  2. Do I want to use Lua to verify passwords that users authenticate with for IMAP/POP3/SMTP in their mail clients? Yes. Password authentication is done through Lua against Nextcloud.
  3. If the answer is 'yes' to question 1 or 2: are there other methods that better facilitate my use case instead of custom scripts which rely on me being a developer and not just a user? No. Only HTTP can be used to authenticate against Nextcloud, which is not supported natively by Dovecot or DMS.

While it is possible to extend the authentication methods which Nextcloud can facilitate with Nextcloud apps, there is currently a mismatch between what DMS supports and what Nextcloud applications can provide. This might change in the future. For now, Lua will be used to bridge the gap between DMS and Nextcloud for authentication only (Dovecot passdb), while LDAP will still be used to identify mailboxes and verify authorization (Dovecot userdb).

"},{"location":"examples/use-cases/auth-lua/#modify-dovecots-configuration","title":"Modify Dovecot's configuration","text":"Add to DMS volumes in compose.yaml
    # All new volumes are marked :ro to configure them as read-only, since their contents are not changed from inside the container\nvolumes:\n# Configuration override to disable LDAP authentication\n- ./docker-data/dms/config/dovecot/auth-ldap.conf.ext:/etc/dovecot/conf.d/auth-ldap.conf.ext:ro\n# Configuration addition to enable Lua authentication\n- ./docker-data/dms/config/dovecot/auth-lua-httpbasic.conf:/etc/dovecot/conf.d/auth-lua-httpbasic.conf:ro\n# Directory containing Lua scripts\n- ./docker-data/dms/config/dovecot/lua/:/etc/dovecot/lua/:ro\n

Create a directory for Lua scripts:

mkdir -p ./docker-data/dms/config/dovecot/lua\n

Create configuration file ./docker-data/dms/config/dovecot/auth-ldap.conf.ext for LDAP user provisioning:

userdb {\n  driver = ldap\n  args = /etc/dovecot/dovecot-ldap.conf.ext\n}\n

Create configuration file ./docker-data/dms/config/dovecot/auth-lua-httpbasic.conf for Lua user authentication:

passdb {\n  driver = lua\n  args = file=/etc/dovecot/lua/auth-httpbasic.lua blocking=yes\n}\n

That is all for configuring Dovecot.

"},{"location":"examples/use-cases/auth-lua/#create-the-lua-script","title":"Create the Lua script","text":"

Create Lua file ./docker-data/dms/config/dovecot/lua/auth-httpbasic.lua with contents:

local http_url = \"https://nextcloud.example.com/remote.php/dav/\"\nlocal http_method = \"PROPFIND\"\nlocal http_status_ok = 207\nlocal http_status_failure = 401\nlocal http_header_forwarded_for = \"X-Forwarded-For\"\n\npackage.path = package.path .. \";/etc/dovecot/lua/?.lua\"\nlocal base64 = require(\"base64\")\n\nlocal http_client = dovecot.http.client {\n  timeout = 1000;\n  max_attempts = 1;\n  debug = false;\n}\n\nfunction script_init()\n  return 0\nend\n\nfunction script_deinit()\nend\n\nfunction auth_passdb_lookup(req)\n  local auth_request = http_client:request {\n    url = http_url;\n    method = http_method;\n  }\n  auth_request:add_header(\"Authorization\", \"Basic \" .. (base64.encode(req.user .. \":\" .. req.password)))\n  auth_request:add_header(http_header_forwarded_for, req.remote_ip)\n  local auth_response = auth_request:submit()\n  local resp_status = auth_response:status()\n  local reason = auth_response:reason()\n\n  local returnStatus = dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE\n  local returnDesc = http_method .. \" - \" .. http_url .. \" - \" .. resp_status .. \" \" .. reason\n  if resp_status == http_status_ok\n  then\n    returnStatus = dovecot.auth.PASSDB_RESULT_OK\n    returnDesc = \"nopassword=y\"\n  elseif resp_status == http_status_failure\n  then\n    returnStatus = dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH\n    returnDesc = \"\"\n  end\n  return returnStatus, returnDesc\nend\n

Replace the hostname in the URL to the actual hostname of Nextcloud.

Dovecot provides an HTTP client for use in Lua. Aside of that, Lua by itself is pretty barebones. It chooses library compactness over included functionality. You can see that in that a separate library is referenced to add support for Base64 encoding, which is required for HTTP basic access authentication. This library (also a Lua script) is not included. It must be downloaded and stored in the same directory:

cd ./docker-data/dms/config/dovecot/lua\ncurl -JLO https://raw.githubusercontent.com/iskolbin/lbase64/master/base64.lua\n

Only use native (pure Lua) libraries as dependencies if possible, such as base64.lua from the example. This ensures maximum compatibility. Performance is less of an issue since Lua scripts written for Dovecot probably won't be long or complex, and there won't be a lot of data processing by Lua itself.

"},{"location":"examples/use-cases/auth-lua/#debugging-a-lua-script","title":"Debugging a Lua script","text":"

To see which Lua version is used by Dovecot if you plan to do something that is version dependent, run:

docker exec CONTAINER_NAME strings /usr/lib/dovecot/libdovecot-lua.so|grep '^LUA_'\n

While Dovecot logs the status of authentication attempts for any passdb backend, Dovecot will also log Lua scripting errors and messages sent to Dovecot's Lua API log functions. The combined DMS log (including that of Dovecot) can be viewed using docker logs CONTAINER_NAME. If the log is too noisy (due to other processes in the container also logging to it), docker exec CONTAINER_NAME cat /var/log/mail/mail.log can be used to view the log of Dovecot and Postfix specifically.

If working with HTTP in Lua, setting debug = true; when initiating dovecot.http.client will create debug log messages for every HTTP request and response.

Note that Lua runs compiled bytecode, and that scripts will be compiled when they are initially started. Once compiled, the bytecode is cached and changes in the Lua script will not be processed automatically. Dovecot will reload its configuration and clear its cached Lua bytecode when running docker exec CONTAINER_NAME dovecot reload. A (changed) Lua script will be compiled to bytecode the next time it is executed after running the Dovecot reload command.

"},{"location":"examples/use-cases/forward-only-mailserver-with-ldap-authentication/","title":"Use Cases | Forward-Only Mail Server with LDAP","text":""},{"location":"examples/use-cases/forward-only-mailserver-with-ldap-authentication/#building-a-forward-only-mail-server","title":"Building a Forward-Only Mail Server","text":"

A forward-only mail server 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 DMS is installed), using as sender any of the alias addresses.

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

PERMIT_DOCKER=host\nENABLE_POP3=\nENABLE_CLAMAV=0\nSMTP_ONLY=1\nENABLE_SPAMASSASSIN=0\nENABLE_FETCHMAIL=0\n

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>\n
"},{"location":"examples/use-cases/forward-only-mailserver-with-ldap-authentication/#authenticating-with-ldap","title":"Authenticating with LDAP","text":"

If you want to send emails from outside the mail server 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:

ACCOUNT_PROVISIONER=LDAP\nLDAP_START_TLS=yes\nLDAP_SERVER_HOST=ldap.example.org\nLDAP_SEARCH_BASE=ou=users,dc=example,dc=org\nLDAP_BIND_DN=cn=mailserver,dc=example,dc=org\nLDAP_BIND_PW=pass1234\n\nENABLE_SASLAUTHD=1\nSASLAUTHD_MECHANISMS=ldap\nSASLAUTHD_LDAP_SERVER=ldap.example.org\nSASLAUTHD_LDAP_START_TLS=yes\nSASLAUTHD_LDAP_BIND_DN=cn=mailserver,dc=example,dc=org\nSASLAUTHD_LDAP_PASSWORD=pass1234\nSASLAUTHD_LDAP_SEARCH_BASE=ou=users,dc=example,dc=org\nSASLAUTHD_LDAP_FILTER=(&(uid=%U)(objectClass=inetOrgPerson))\n

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\nuid: username\nobjectClass: inetOrgPerson\nsn: username\ncn: username\nuserPassword: {SSHA}abcdefghi123456789\nemail: external-account@gmail.com\n

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

#!/bin/bash\n\nrm -f /etc/postfix/{ldap-groups.cf,ldap-domains.cf}\n\npostconf \\\n\"virtual_mailbox_domains = /etc/postfix/vhost\" \\\n\"virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf texthash:/etc/postfix/virtual\" \\\n\"smtpd_sender_login_maps = ldap:/etc/postfix/ldap-users.cf\"\n\nsed -i /etc/postfix/ldap-users.cf \\\n-e '/query_filter/d' \\\n-e '/result_attribute/d' \\\n-e '/result_format/d'\ncat <<EOF >> /etc/postfix/ldap-users.cf\nquery_filter = (uid=%u)\nresult_attribute = uid\nresult_format = %s@example.org\nEOF\n\nsed -i /etc/postfix/ldap-aliases.cf \\\n-e '/domain/d' \\\n-e '/query_filter/d' \\\n-e '/result_attribute/d'\ncat <<EOF >> /etc/postfix/ldap-aliases.cf\ndomain = example.org\nquery_filter = (uid=%u)\nresult_attribute = mail\nEOF\n\npostfix reload\n

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

See also

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 mail server is this.

"},{"location":"examples/use-cases/imap-folders/","title":"Mailboxes (aka IMAP Folders)","text":"

INBOX is setup as the private inbox namespace. By default target/dovecot/15-mailboxes.conf configures the special IMAP folders Drafts, Sent, Junk and Trash to be automatically created and subscribed. They are all assigned to the private inbox namespace (which implicitly provides the INBOX folder).

These IMAP folders are considered special because they add a \"SPECIAL-USE\" attribute, which is a standardized way to communicate to mail clients that the folder serves a purpose like storing spam/junk mail (\\Junk) or deleted mail (\\Trash). This differentiates them from regular mail folders that you may use for organizing.

"},{"location":"examples/use-cases/imap-folders/#adding-a-mailbox-folder","title":"Adding a mailbox folder","text":"

See target/dovecot/15-mailboxes.conf for existing mailbox folders which you can modify or uncomment to enable some other common mailboxes. For more information try the official Dovecot documentation.

The Archive special IMAP folder may be useful to enable. To do so, make a copy of target/dovecot/15-mailboxes.conf and uncomment the Archive mailbox definition. Mail clients should understand that this folder is intended for archiving mail due to the \\Archive \"SPECIAL-USE\" attribute.

With the provided compose.yaml example, a volume bind mounts the host directory docker-data/dms/config/ to the container location /tmp/docker-mailserver/. Config file overrides should instead be mounted to a different location as described in Overriding Configuration for Dovecot:

volumes:\n- ./docker-data/dms/config/dovecot/15-mailboxes.conf:/etc/dovecot/conf.d/15-mailboxes.conf:ro\n
"},{"location":"examples/use-cases/imap-folders/#caution","title":"Caution","text":""},{"location":"examples/use-cases/imap-folders/#adding-folders-to-an-existing-setup","title":"Adding folders to an existing setup","text":"

Handling of newly added mailbox folders can be inconsistent across mail clients:

  • Users may experience issues such as archived emails only being available locally.
  • Users may need to migrate emails manually between two folders.
"},{"location":"examples/use-cases/imap-folders/#support-for-special-use-attributes","title":"Support for SPECIAL-USE attributes","text":"

Not all mail clients support the SPECIAL-USE attribute for mailboxes (defined in RFC 6154). These clients will treat the mailbox folder as any other, using the name assigned to it instead.

Some clients may still know to treat these folders for their intended purpose if the mailbox name matches the common names that the SPECIAL-USE attributes represent (eg Sent as the mailbox name for \\Sent).

"},{"location":"examples/use-cases/imap-folders/#internationalization-i18n","title":"Internationalization (i18n)","text":"

Usually the mail client will know via context such as the SPECIAL-USE attribute or common English mailbox names, to provide a localized label for the users preferred language.

Take care to test localized names work well as well.

"},{"location":"examples/use-cases/imap-folders/#email-clients-support","title":"Email Clients Support","text":"
  • If a new mail account is added without the SPECIAL-USE attribute enabled for archives:
    • Thunderbird suggests and may create an Archives folder on the server.
    • Outlook for Android archives to a local folder.
    • Spark for Android archives to server folder named Archive.
  • If a new mail account is added after the SPECIAL-USE attribute is enabled for archives:
    • Thunderbird, Outlook for Android and Spark for Android will use the mailbox folder name assigned.

Windows Mail

Windows Mail has been said to ignore SPECIAL-USE attribute and look only at the mailbox folder name assigned.

Needs citation

This information is provided by the community.

It presently lacks references to confirm the behaviour. If any information is incorrect please let us know!

"},{"location":"examples/use-cases/ios-mail-push-support/","title":"Advanced | iOS Mail Push Support","text":""},{"location":"examples/use-cases/ios-mail-push-support/#introduction","title":"Introduction","text":"

iOS Mail currently does not support the IMAP idle extension. Therefore users can only either check manually or configure intervals for fetching mails in their mail account preferences when using the default configuration.

To support mail push Dovecot needs to advertise the XAPPLEPUSHSERVICE IMAP extension as well as sending the actual push notifications to the Apple Push Notification service (APNs) which will forward them to the device.

This can be done with two components:

  • A Dovecot plugin (dovecot-xaps-plugin) which is triggered whenever a mail is created or moved from/to a mail folder.
  • A daemon service (dovecot-xaps-daemon) that manages both the device registrations as well as sending notifications to the APNs.
"},{"location":"examples/use-cases/ios-mail-push-support/#prerequisites","title":"Prerequisites","text":"
  • An Apple developer account to create the required Apple Push Notification service certificate.
  • Knowledge creating Docker images, using the command-line, and creating shell scripts.
"},{"location":"examples/use-cases/ios-mail-push-support/#limitations","title":"Limitations","text":"
  • You need to maintain a custom docker-mailserver image.
  • Push support is limited to the INBOX folder. Changes to other folders will not be pushed to the device regardless of the configuration settings.
  • You currently cannot use the same account UUID on multiple devices. This means that if you use the same backup on multiple devices (e.g. old phone / new phone) only one of them will get the notification. Use different backups or recreate the mail account.
"},{"location":"examples/use-cases/ios-mail-push-support/#privacy-concerns","title":"Privacy concerns","text":"
  • The service does not send any part of the actual message to Apple.
  • The information sent contains the device UUID to notify and the (on-device) account UUID which was generated by the iOS mail application when creating the account.
  • Upon receiving the notification, the iOS mail application will connect to the IMAP server given by the provided account UUID and fetch the mail to notify the user.
  • Apple therefore does not know the mail address for which the mail was received, only that a specific account on a specific device should be notified that a new mail or that a mail was moved to the INBOX folder.
"},{"location":"examples/use-cases/ios-mail-push-support/#installation","title":"Installation","text":"

Both components will be built using Docker and included into a custom docker-mailserver image. Afterwards the required configuration is added to docker-data/dms/config. The registration data is stored in /var/mail-state/lib-xapsd.

  1. Create a Dockerfile to build a docker-mailserver image that includes the dovecot-xaps-plugin as well as the dovecot-xaps-daemon. This is required to ensure that the Dovecot plugin is built against the same Dovecot version. The :edge tag is used here, but you might want to use a released version instead.

    FROM mailserver/docker-mailserver:edge AS dovecot-plugin-xaps\nWORKDIR /tmp/dovecot-xaps-plugin\nRUN <<EOF\n    apt-get update\n    apt-get -y --no-install-recommends install git cmake make build-essential dovecot-dev\n    git clone --single-branch --depth=1 https://github.com/freswa/dovecot-xaps-plugin.git .\n    mkdir build && cd build\n    cmake .. -DCMAKE_BUILD_TYPE=Release\n    make install\nEOF\n\n# Use an older Go version as Go >= 1.20 causes this issue: https://github.com/freswa/dovecot-xaps-daemon/issues/24#issuecomment-1483876081\n# Note that the underlying issue are non-standard-compliant Apple http servers which might get fixed at some point\nFROM golang:1.19-alpine AS dovecot-xaps-daemon\nENV GOPROXY=https://proxy.golang.org,direct\nENV CGO_ENABLED=0\nWORKDIR /go/dovecot-xaps-daemon\nRUN <<EOF\n    apk add --no-cache --virtual build-dependencies git\n    git clone --single-branch --depth=1 https://github.com/freswa/dovecot-xaps-daemon .\n    go build ./cmd/xapsd\nEOF\n\nFROM mailserver/docker-mailserver:edge\nCOPY --from=dovecot-plugin-xaps /usr/lib/dovecot/modules/*_xaps_* /usr/lib/dovecot/modules/\nCOPY --from=dovecot-xaps-daemon /go/dovecot-xaps-daemon/xapsd /usr/bin/xapsd\n\n# create a non-root user for the daemon process as well as configuration and run state directories\nRUN <<EOF\n    adduser --quiet --system --group --disabled-password --home /var/mail-state/lib-xapsd --no-create-home xapsd\n    mkdir -p /var/run/xapsd /etc/xapsd\nEOF\n
  2. Build the new image:

    docker build -t yourname/docker-mailserver .\n

  3. Modify your compose.yaml to use the newly created image:

        services:\nmailserver:\nimage: yourname/docker-mailserver:latest\n

  4. Recreate the container:

    docker compose down\ndocker compose up -d\n

  5. Create a hash of your Apple developer account password using the provided xapsd -pass command:

    docker exec -it mailserver xapsd -pass\n

  6. Add configuration for both components:

    • Create a folder named xaps in docker-data/dms/config.

    • Create a file named xapsd.yaml in docker-data/dms/config/xaps.

      • Replace appleId and appleIdHashedPassword with your actual credentials. For reference see also here.
      • The service will use the provided username/hash combination to automatically request a new certificate from Apple as well as renewing an older certificate if needed.
      xapsd.yaml
      # set the loglevel to either\n# trace, debug, error, fatal, info, panic or warn\n# Default: info\nloglevel: info\n\n# xapsd creates a json file to store the registration persistent on disk.\n# This sets the location of the file.\ndatabaseFile: /var/mail-state/lib-xapsd/database.json\n\n# xapsd listens on a socket for http/https requests from the dovecot plugin.\n# This sets the address and port number of the listen socket.\nlistenAddr: '127.0.0.1'\nport: 11619\n\n# xapsd is able to listen on a HTTPS Socket to allow HTTP/2 to be used\n# SSL is enabled implicitly when certfile and keyfile exist\n# !!! only use HTTPS for connection pooling with a proxy e.g. nginx or HaProxy\n# !!! direct usage with the plugin is discouraged and unsupported\ntlsCertfile:\ntlsKeyfile:\ntlsListenAddr:\ntlsPort: 11620\n\n# Notifications that are not initiated by new messages are not sent immediately for two reasons:\n# 1. When you move/copy/delete messages you most likely move/copy/delete more messages within a short period of time.\n# 2. You don't need your mailboxes to synchronize immediately since they are automatically synchronized when opening\n#    the app\n# If a new message comes and the move/copy/delete notification is still on hold it will be sent with the notification\n# for the new message.\n# This sets the interval to check for delayed messages.\ncheckInterval: 20\n\n# Set the time how long notifications for not-new messages should be delayed until they are sent.\n# Whenever checkInterval runs, it checks if \"delay\" <= \"waiting time\" and sends the notification if the expression is\n# true.\ndelay: 30\n\n# To retrieve certificates from Apple, we need to login with a valid Apple ID\n# The accounts email must be given in cleartext, but the password has to\n# be hashed before sending it. To not leak working credentials on running servers,\n# we do not accept the cleartext password here.\nappleId: foo@example.com\n\n# use `xaps -pass` to calculate the hash of the apple id password\nappleIdHashedPassword: bar\n
    • Create a file named 95-xaps.conf in docker-data/dms/config/xaps. For reference see also here. 95-xaps.conf

      protocol imap {\n  mail_plugins = $mail_plugins notify push_notification xaps_push_notification xaps_imap\n}\n\nprotocol lda {\n  mail_plugins = $mail_plugins notify push_notification xaps_push_notification\n}\n\nprotocol lmtp {\n  mail_plugins = $mail_plugins notify push_notification xaps_push_notification\n}\n\nplugin {\n    # xaps_config contains xaps specific configuration parameters\n    # url:              protocol, hostname and port under which xapsd listens\n    # user_lookup: Use if you want to determine the username used for PNs from environment variables provided by\n    #                   login mechanism. Value is variable name to look up.\n    # max_retries:      maximum num of retries the http client connects to the xaps daemon\n    # timeout_msecs     http timeout of the http connection\n    xaps_config = url=http://127.0.0.1:11619 user_lookup=theattribute max_retries=6 timeout_msecs=5000\n    push_notification_driver = xaps\n}\n

    • Create a supervisord file named xapsd.conf in docker-data/dms/config/xaps with the following content: xapsd.conf

      [program:xapsd]\nstartsecs=0\nautostart=false\nautorestart=true\nstdout_logfile=/var/log/supervisor/%(program_name)s.log\nstderr_logfile=/var/log/supervisor/%(program_name)s.log\nuser=xapsd\ncommand=/usr/bin/xapsd\npidfile=/var/run/xapsd/xapsd.pid\n

    • Create or update your user-patches.sh in docker-data/dms/config to move the files to their final location as well as starting the daemon service: user-patches.sh

      #!/bin/bash\n\n# Copy the configs to internal locations:\ncp /tmp/docker-mailserver/xaps/95-xaps.conf /etc/dovecot/conf.d/95-xaps.conf\ncp /tmp/docker-mailserver/xaps/xapsd.yaml /etc/xapsd/xapsd.yaml\ncp /tmp/docker-mailserver/xaps/xapsd.conf /etc/supervisor/conf.d/xapsd.conf\n\n# Setup data persistence and ensure ownership is always for xapsd:\nmkdir -p /var/mail-state/lib-xapsd\nchown -R xapsd:xapsd /var/mail-state/lib-xapsd\n\n# Start the xaps daemon:\nsupervisorctl update\nsupervisorctl start xapsd\n

  7. Recreate the container again to apply the new configuration:

    docker compose down\ndocker compose up -d\n

  8. Recreate your mail account on your iOS device and check the logs in /var/log/supervisor/dovecot.log and /var/log/supervisor/xapsd.log for any errors.

"},{"location":"examples/use-cases/ios-mail-push-support/#other-configuration-options","title":"Other configuration options","text":"

Both device registration and notifications send a username to the daemon to lookup the device. While the registration and other IMAP operations in Dovecot will send the Dovecot username, LMTP will send the provided authentication username.

The format of that username is specified by the auth_username_format Dovecot setting. If you are not using mail addresses as Dovecot usernames - e.g. when using LDAP - you can either change the auth_username_format or add the mail address as property to the user account and use the lookup feature (see below).

user-patches.sh
sed -i -r \"s|^#?(auth_username_format =).*|\\1 %Ln|\" /etc/dovecot/conf.d/10-auth.conf\n

You can also use notifications for Dovecot alias mailboxes. Depending on your server configuration, this might require to add the original Dovecot username as Dovecot attribute to the login user as well as changing the user_lookup=theattribute in 95-xaps.conf to perform the lookup of that attribute.

"}]} \ No newline at end of file diff --git a/v13.1/sitemap.xml b/v13.1/sitemap.xml new file mode 100644 index 00000000..9b4a6e78 --- /dev/null +++ b/v13.1/sitemap.xml @@ -0,0 +1,223 @@ + + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/faq/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/introduction/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/usage/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/debugging/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/environment/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/pop3/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/setup.sh/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/user-management/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/auth-ldap/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/dovecot-master-accounts/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/full-text-search/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/ipv6/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/kubernetes/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/mail-fetchmail/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/mail-getmail/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/mail-sieve/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/optional-config/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/podman/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/mail-forwarding/aws-ses/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/mail-forwarding/relay-hosts/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/maintenance/update-and-cleanup/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/override-defaults/dovecot/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/override-defaults/postfix/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/advanced/override-defaults/user-patches/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/best-practices/autodiscover/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/best-practices/dkim_dmarc_spf/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/security/fail2ban/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/security/mail_crypt/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/security/rspamd/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/security/ssl/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/config/security/understanding-the-ports/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/contributing/general/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/contributing/issues-and-pull-requests/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/contributing/tests/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/examples/tutorials/basic-installation/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/examples/tutorials/blog-posts/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/examples/tutorials/crowdsec/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/examples/tutorials/docker-build/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/examples/tutorials/mailserver-behind-proxy/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/examples/use-cases/auth-lua/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/examples/use-cases/forward-only-mailserver-with-ldap-authentication/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/examples/use-cases/imap-folders/ + 2023-12-29 + daily + + + https://docker-mailserver.github.io/docker-mailserver/v13.1/examples/use-cases/ios-mail-push-support/ + 2023-12-29 + daily + + \ No newline at end of file diff --git a/v13.1/usage/index.html b/v13.1/usage/index.html new file mode 100644 index 00000000..ec2eda47 --- /dev/null +++ b/v13.1/usage/index.html @@ -0,0 +1,2354 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Usage - Docker Mailserver + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

Usage

+ +

This pages explains how to get started with DMS. The guide uses Docker Compose as a reference. In our examples, a volume mounts the host location docker-data/dms/config/ to /tmp/docker-mailserver/ inside the container.

+

Preliminary Steps

+

Before you can get started with deploying your own mail server, there are some requirements to be met:

+
    +
  1. You need to have a host that you can manage.
  2. +
  3. You need to own a domain, and you need to able to manage DNS for this domain.
  4. +
+

Host Setup

+

There are a few requirements for a suitable host system:

+
    +
  1. The host should have a static IP address; otherwise you will need to dynamically update DNS (undesirable due to DNS caching)
  2. +
  3. The host should be able to send/receive on the necessary ports for mail
  4. +
  5. You should be able to set a PTR record for your host; security-hardened mail servers might otherwise reject your mail server as the IP address of your host does not resolve correctly/at all to the DNS name of your server.
  6. +
+
+

About the Container Runtime

+

On the host, you need to have a suitable container runtime (like Docker or Podman) installed. We assume Docker Compose is installed. We have aligned file names and configuration conventions with the latest Docker Compose (currently V2) specification.

+

If you're using podman, make sure to read the related documentation.

+
+

Minimal DNS Setup

+

The DNS setup is a big and essential part of the whole setup. There is a lot of confusion for newcomers and people starting out when setting up DNS. This section provides an example configuration and supplementary explanation. We expect you to be at least a bit familiar with DNS, what it does and what the individual record types are.

+

Now let's say you just bought example.com and you want to be able to send and receive e-mails for the address test@example.com. On the most basic level, you will need to

+
    +
  1. set an MX record for your domain example.com - in our example, the MX record contains mail.example.com
  2. +
  3. set an A record that resolves the name of your mail server - in our example, the A record contains 11.22.33.44
  4. +
  5. (in a best-case scenario) set a PTR record that resolves the IP of your mail server - in our example, the PTR contains mail.example.com
  6. +
+

We will later dig into DKIM, DMARC & SPF, but for now, these are the records that suffice in getting you up and running. Here is a short explanation of what the records do:

+
    +
  • The MX record tells everyone which (DNS) name is responsible for e-mails on your domain. + Because you want to keep the option of running another service on the domain name itself, you run your mail server on mail.example.com. + This does not imply your e-mails will look like test@mail.example.com, the DNS name of your mail server is decoupled of the domain it serves e-mails for. + In theory, you mail server could even serve e-mails for test@some-other-domain.com, if the MX record for some-other-domain.com points to mail.example.com.
  • +
  • The A record tells everyone which IP address the DNS name mail.example.com resolves to.
  • +
  • The PTR record is the counterpart of the A record, telling everyone what name the IP address 11.22.33.44 resolves to.
  • +
+
+

About The Mail Server's Fully Qualified Domain Name

+

The mail server's fully qualified domain name (FQDN) in our example above is mail.example.com. Please note though that this is more of a convention, and not due to technical restrictions. One could also run the mail server

+
    +
  1. on foo.example.com: you would just need to change your MX record;
  2. +
  3. on example.com directly: you would need to change your MX record and probably read our docs on bare domain setups, as these setups are called "bare domain" setups.
  4. +
+

The FQDN is what is relevant for TLS certificates, it has no (inherent/technical) relation to the email addresses and accounts DMS manages. That is to say: even though DMS runs on mail.example.com, or foo.example.com, or example.com, there is nothing that prevents it from managing mail for barbaz.org - barbaz.org will just need to set its MX record to mail.example.com (or foo.example.com or example.com).

+
+

If you setup everything, it should roughly look like this:

+
$ dig @1.1.1.1 +short MX example.com
+mail.example.com
+$ dig @1.1.1.1 +short A mail.example.com
+11.22.33.44
+$ dig @1.1.1.1 +short -x 11.22.33.44
+mail.example.com
+
+

Deploying the Actual Image

+

Tagging Convention

+

To understand which tags you should use, read this section carefully. Our CI will automatically build, test and push new images to the following container registries:

+
    +
  1. DockerHub (docker.io/mailserver/docker-mailserver)
  2. +
  3. GitHub Container Registry (ghcr.io/docker-mailserver/docker-mailserver)
  4. +
+

All workflows are using the tagging convention listed below. It is subsequently applied to all images.

+ + + + + + + + + + + + + + + + + +
EventImage Tags
push on masteredge
push a tag (v1.2.3)1.2.3, 1.2, 1, latest
+

Get All Files

+

Issue the following commands to acquire the necessary files:

+
DMS_GITHUB_URL="https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master"
+wget "${DMS_GITHUB_URL}/compose.yaml"
+wget "${DMS_GITHUB_URL}/mailserver.env"
+
+

Configuration Steps

+
    +
  1. First edit compose.yaml to your liking
      +
    • Substitute mail.example.com according to your FQDN.
    • +
    • If you want to use SELinux for the ./docker-data/dms/config/:/tmp/docker-mailserver/ mount, append -z or -Z.
    • +
    +
  2. +
  3. Then configure the environment specific to the mail server by editing mailserver.env, but keep in mind that:
      +
    • only basic VAR=VAL is supported
    • +
    • do not quote your values
    • +
    • variable substitution is not supported, e.g. OVERRIDE_HOSTNAME=$HOSTNAME.$DOMAINNAME does not work
    • +
    +
  4. +
+

Get Up and Running

+
+

Using the Correct Commands For Stopping and Starting DMS

+

Use docker compose up / down, not docker compose start / stop. Otherwise, the container is not properly destroyed and you may experience problems during startup because of inconsistent state.

+

Using Ctrl+C is not supported either!

+
+

For an overview of commands to manage DMS config, run: docker exec -it <CONTAINER NAME> setup help.

+
+Usage of setup.sh when no DMS Container Is Running +

We encourage you to directly use setup inside the container (like shown above). If you still want to use setup.sh, here's some information about it.

+

If no DMS container is running, any ./setup.sh command will check online for the :latest image tag (the current stable release), performing a docker pull ... if necessary followed by running the command in a temporary container:

+
$ ./setup.sh help
+Image 'ghcr.io/docker-mailserver/docker-mailserver:latest' not found. Pulling ...
+SETUP(1)
+
+NAME
+    setup - 'docker-mailserver' Administration & Configuration script
+...
+
+$ docker run --rm ghcr.io/docker-mailserver/docker-mailserver:latest setup help
+SETUP(1)
+
+NAME
+    setup - 'docker-mailserver' Administration & Configuration script
+...
+
+
+

On first start, you will need to add at least one email account (unless you're using LDAP). You have two minutes to do so, otherwise DMS will shutdown and restart. You can add accounts by running docker exec -ti <CONTAINER NAME> setup email add user@example.com. That's it! It really is that easy.

+

Further Miscellaneous Steps

+

Setting up TLS

+

You definitely want to setup TLS. Please refer to our documentation about TLS.

+

Aliases

+

You should add at least one alias, the postmaster alias. This is a common convention, but not strictly required.

+
docker exec -ti <CONTAINER NAME> setup alias add postmaster@example.com user@example.com
+
+

Advanced DNS Setup - DKIM, DMARC & SPF

+

You will very likely want to configure your DNS with these TXT records: SPF, DKIM, and DMARC. We also ship a dedicated page in our documentation about the setup of DKIM, DMARC & SPF.

+

Custom User Changes & Patches

+

If you'd like to change, patch or alter files or behavior of DMS, you can use a script. See this part of our documentation for a detailed explanation.

+

Testing

+

Here are some tools you can use to verify your configuration:

+
    +
  1. MX Toolbox
  2. +
  3. DMARC Analyzer
  4. +
  5. mail-tester.com
  6. +
  7. multiRBL.valli.org
  8. +
  9. internet.nl
  10. +
+ + + + + + +
+
+ + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file