{"id":2450,"date":"2012-02-26T18:23:32","date_gmt":"2012-02-26T17:23:32","guid":{"rendered":"http:\/\/www.devco.net\/?p=2450"},"modified":"2012-03-07T14:55:32","modified_gmt":"2012-03-07T13:55:32","slug":"managing-email-forwarding-in-exim-with-puppet","status":"publish","type":"post","link":"https:\/\/www.devco.net\/archives\/2012\/02\/26\/managing-email-forwarding-in-exim-with-puppet.php","title":{"rendered":"Managing email forwarding in Exim with Puppet"},"content":{"rendered":"

I have a number of mail servers where mail enters, get spam scanned etc and then forwarded to mail box servers. This used to be customer facing and had web interfaces and statistics etc but I am now scaling all this down to just manage my own and some friends domains.<\/p>\n

Rather than maintain all the web interfaces that I really could not care for I’d rather manage this with Puppet, my ideal end result would be:<\/p>\n

<\/p>\n

\r\nexim::route{\"devco.net\":\r\n  nexthop         => \"my.mailbox.server\",\r\n  spamthreshold   => 10,\r\n  spamdestination => \":blackhole:\",\r\n  has_greylist    => 1,\r\n  has_spam_check  => 1,\r\n  has_whitelist   => 1\r\n}\r\n<\/pre>\n

<\/code><\/p>\n

This should add all the required configuration to deliver mail arriving at the mail relay for devco.net<\/em> to the server my.mailbox.server<\/em>. It will set up Spam Assassin scans and send all mail that scores more than 10 to the exim specific destination :blackhole:<\/em> that would simply delete the mail. I could specify any valid mail destination here like a file or other email address. I won’t be covering the has_* entries in this guide, they just control various policies in my ACLs on a per domain basis.<\/p>\n

I’ll first cover the Exim side of things, clearly I do not want to be editing exim.conf each time so I will read the domain information from a file stored on the server. These files will be stored in \/etc\/exim\/routes\/devco.net<\/em> and look like:<\/p>\n

<\/p>\n

\r\nnexthop: my.mailbox.server\r\nspamthreshold: 10\r\nspamdestination: :blackhole:\r\n<\/pre>\n

<\/code><\/p>\n

In order to accept mail for a domain Exim needs a list of valid domains it will accept mail for, so as our routes are named after the domain we can just leverage that to build the list:<\/p>\n

<\/p>\n

\r\ndomainlist mw_domains = dsearch;\/etc\/exim\/routes\r\n<\/pre>\n

<\/code><\/p>\n

Next we should pull from the file the various settings we store there:<\/p>\n

<\/p>\n

\r\nNEXTHOP = ${lookup{nexthop}lsearch{\/etc\/exim\/routes\/${domain}}}\r\nDOMAINREJECTSCORE = ${eval:10*${lookup{spamthreshold}lsearch{\/etc\/exim\/routes\/${domain}}}}\r\nDOMAINSPAMDEST = ${lookup{spamdestination}lsearch{\/etc\/exim\/routes\/${domain}}}\r\n\r\nACL_SPAMSCORE = acl_m3\r\n<\/pre>\n

<\/code><\/p>\n

This creates handy variables that we can just use in our routes and spam configuration, I won’t go into the actual setup of spam assassin scanning as that’s pretty standard stuff better documented elsewhere. In the spam assassin ACLs just store your $spam_score_int<\/em> in ACL_SPAMSCORE<\/em>.<\/p>\n

To deliver the mail either to the specific spam destination or to the next hop we just need to add 2 routers to the routes section. These are order dependant so they should be in the order below:<\/p>\n

<\/p>\n

\r\nspamblock:\r\n  driver          = redirect\r\n  condition       = ${if >= {$ACL_SPAMSCORE}{DOMAINREJECTSCORE}{true}{false}}\r\n  data            = DOMAINSPAMDEST\r\n  headers_add     = X-MW-Note: Redirecting mail to domain spam destination\r\n  domains         = +mw_domains\r\n  no_verify\r\n<\/pre>\n

<\/code><\/p>\n

Here we’re just doing a quick if check over the stored spam score to see if its bigger or equal to the threshold stored in DOMAINREJECTSCORE<\/em> and then set the data of the route – where the mail should go – to the configured address from DOMAINSPAMDEST<\/em>. This router will only be active for domains that this Exim server is a relay for and it adds a little debug note as a header.<\/p>\n

The actual mail delivery that is being used in place of the normal dnslookup<\/em> route is here:<\/p>\n

<\/p>\n

\r\nmw_domains:\r\n  driver          = manualroute\r\n  transport       = remote_smtp\r\n  domains         = +mw_domains\r\n  user            = root\r\n  headers_add     = \"X-MW-Recipient: ${local_part}@${domain}\\n\\\r\n                     X-MW-Sender: $sender_address\\n\\\r\n                     X-MW-Server: $primary_hostname\"\r\n  route_data      = MW_NEXTHOP\r\n<\/pre>\n

<\/code><\/p>\n

This router is also restricted to only our relay domains, it adds some headers for debug purposes and finally sets the route_data<\/em> of the email to the next hop from MW_NEXTHOP<\/em> thus delivering the mail to the destination.<\/p>\n

That’s all there is to do on the Exim side, it’s pretty standard stuff. Next up the Puppet define:<\/p>\n

<\/p>\n

\r\ndefine exim::route($nexthop, $spamthreshold, $spamdestination, $ensure = \"present\") {\r\n  file{\"\/etc\/exim\/routes\/${name}\":\r\n    ensure  => $ensure,\r\n    content => template(\"exim\/route.erb\")\r\n  }\r\n}\r\n<\/pre>\n

<\/code><\/p>\n

And the template for this define is also extremely simple:<\/p>\n

<\/p>\n

\r\nnexthop: <%= nexthop %>\r\nspamthreshold: <%= spamthreshold %>\r\nspamdestination: <%= spamdestination %>\r\n<\/pre>\n

<\/code><\/p>\n

I could stop here and just create a bunch of exim::route<\/em> resources but that would be code changes, I prefer just changing data. So I am going to create a JSON file called mailrelay.json<\/em> and store it with my Hiera data.<\/p>\n

<\/p>\n

\r\n{\r\n  \"relay_domains\": {\r\n    \"devco.net\": {\r\n      \"nexthop\": \"my.mailbox.server\",\r\n      \"spamdestination\": \":blackhole:\",\r\n      \"spamthreshold\": 10,\r\n      \"has_dkim\": 1\r\n    },\r\n    \"another.com\": {\r\n      \"nexthop\": \"ASPMX.L.GOOGLE.COM.\",\r\n      \"spamdestination\": \":blackhole:\",\r\n      \"spamthreshold\": 10\r\n    }\r\n  }\r\n}\r\n<\/pre>\n

<\/code><\/p>\n

I assign all my incoming mail servers a single class that would look roughly like this:<\/p>\n

<\/p>\n

\r\nclass roles::mailrelay {\r\n  include exim\r\n  include exim::mailrelay\r\n\r\n  $routes = hiera(\"relay_domains\", \"\", \"mailrelay\")\r\n  $domains = keys($routes)\r\n\r\n  exim::routemap{$domains:\r\n    routes => $routes\r\n  }\r\n}\r\n<\/pre>\n

<\/code><\/p>\n

The call to Hiera fetches the entire hash from the mailrelay.json<\/em> file and stores it in $routes<\/em>. I then use the keys<\/em> function from puppetlabs-stdlib<\/a> to extract just the list of domains into an array. I then pass that into a define exim::routemap<\/em> that iterates the list and builds up individual exim::route<\/em> resources.<\/p>\n

The routemap define is just as below, I’ve shortened it a fair bit as I also have validation logic in here to make sure I pass valid data in the hash from Hiera, the stdlib module has various validator functions thats really handy for this:<\/p>\n

<\/p>\n

\r\ndefine exim::routemap($routes) {\r\n  exim::route{$name:\r\n    nexthop => $routes[$name][\"nexthop\"],\r\n    spamthreshold => $routes[$name][\"spamthreshold\"],\r\n    spamdestination => $routes[$name][\"spamdestination\"]\r\n  }\r\n\r\n  if ($routes[$name][\"has_dkim\"] == 1) {\r\n    exim::dkim_domain{$name: }\r\n  } else {\r\n    exim::dkim_domain{$name: ensure => absent}\r\n  }\r\n}\r\n<\/pre>\n

<\/code><\/p>\n

And that’s about it, now my mail routing setup, DKIM signing and other policies are managed in a simple JSON file in my Puppet Manifests.<\/p>\n","protected":false},"excerpt":{"rendered":"

I have a number of mail servers where mail enters, get spam scanned etc and then forwarded to mail box servers. This used to be customer facing and had web interfaces and statistics etc but I am now scaling all this down to just manage my own and some friends domains. Rather than maintain all […]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_et_pb_use_builder":"","_et_pb_old_content":"","footnotes":""},"categories":[7],"tags":[82,97,21],"_links":{"self":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts\/2450"}],"collection":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/comments?post=2450"}],"version-history":[{"count":11,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts\/2450\/revisions"}],"predecessor-version":[{"id":2462,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/posts\/2450\/revisions\/2462"}],"wp:attachment":[{"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/media?parent=2450"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/categories?post=2450"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devco.net\/wp-json\/wp\/v2\/tags?post=2450"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}