IMAP-magic, or iMapic?…yes?…no?….

I was given an opportunity to setup an email system for a friend that is similar to mine, so I figured I’d document this a bit better than what i’ve been documenting before.

As a primer, this is how we use sendmail, dovecot, and php to get email at a host and have it automatically move messages between email inboxes. Fairly easy 🙂

This should be a bit better than my previous partial writeups, specifically email.heick.email, sendmail & dovecot, how do you work…, and Dovecot IMAP (part 1).

Table of Contents:

  1. Base System Setup
  2. Software Installation
  3. Dovecot IMAP Configuration
    1. Users
    2. Configuration
    3. Kickoff
    4. Debugging
  4. Sendmail Configuration
    1. local-host-names
    2. virtusertable
    3. sendmail.mc
    4. Kickoff
    5. Testing
  5. Service Wrap-up
  6. PHP Automation

Base System Setup

To make sure I don’t futz anything up I decided to do a kick-off session in VirtualBox with a Centos 6.9 base. I used the minimal installation media, setup the domain, activated the networking with DHCP, set the root password, and got things underway.

After installation was done and the OS was rebooted, I proceeded to create myself a user, visudo that user for godness, set selinux to permissive, and reboot. Some standard maintenance before we proceed with awesomeness.

The commands are pretty much copypasta. As root:

# adduser matt
# passwd matt
Changing password for user matt.
New password:
BAD PASSWORD: it is based on a dictionary word
Retype new password:
passwd: all authentication tokens updated successfully.
# visudo
...
## Allow root to run any commands anywhere
root    ALL=(ALL)       ALL
matt ALL=(ALL) ALL
# shutdown -r now

As myself:

$ uname -a
Linux niffynoo.vm 2.6.32-696.el6.x86_64 #1 SMP Tue Mar 21 19:29:05 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ sudo vi /etc/selinux/config
SELINUX=permissive
$ sudo yum update
...tons of yum update stuff
$ sudo shutdown -r now
...shutdown+restart
$ uname -a
Linux niffynoo.vm 2.6.32-696.23.1.el6.x86_64 #1 SMP Tue Mar 13 22:44:18 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Software Installation

We know from previous write-ups that we have a base of software to install that will get us our mail server capabilities.

$ sudo yum install dovecot sendmail cyrus-sasl sendmail-cf cyrus-sasl-devel cyrus-sasl-gssapi cyrus-sasl-md5 cyrus-sasl-plain
...yum installation stuff
$ sudo service sendmail status
sendmail is stopped
sm-client is stopped
$ sudo service dovecot status
dovecot is stopped
$ sudo service saslauthd status
saslauthd is stopped

Dovecot IMAP Configuration

Dovecots configuration for what we need it to do is fairly straightforward, as we want the *@domain.com setup. All email going to the inbox will eventually be scripted out and delivered to a specific IMAP folder for easy sorting.

Users

Firstly, we’ll need a system-level user that has a home folder. This specific user is simply for permissions and resource segregation. Since we’re on this whole “magic”-imap deal, we’ll keep with the imapic setup. In a real-world scenario

$ sudo adduser imapic
$ sudo passwd imapic
Changing password for user imapic.
New password: password
BAD PASSWORD: it is based on a dictionary word
Retype new password: password
passwd: all authentication tokens updated successfully.

Secondly, we’re gonna go the cheap route and create a passwd file that allows the user to login with a plaintext username/password and associate themselves with the system user. It is important in this case to duplicate the uid/gid from the system user we’ve created in the previous example. We’ll manufacture the file in /etc/dovecot as users

$ id imapic
uid=501(imapic) gid=501(imapic) groups=501(imapic)
$ sudo touch /etc/dovecot/users
$ sudo su
# echo "imapic:{PLAIN}derp:501:501::/home/imapic" > /etc/dovecot/users

So, now there is a system user named imapic with the password password, and a dovecot user with the same name but a password of derp.

We now need a configuration for dovecot to read the users mail as their inbox.

Configuration

All configurations for dovecot is in /etc/dovecot/conf.d. We’ll just copy/paste our defacto config into there.

For the sake of config loading and naming, we’ll call this 99-awesome.conf

# This enables imap(143) and imaps(993)
protocols = imap pop3

# Setup where we get and store mail at
mail_location = mbox:~/mail:INBOX=/var/mail/%u

# related to authentication
disable_plaintext_auth = no
auth_mechanisms = plain

# Using a passwordfile
!include auth-passwdfile.conf.ext

# Set so we can use this from home from more than one device
mail_max_userip_connections = 50

The config is actually very easy to read:

  1. Setup protocols to read mail
  2. Setup where we store IMAP folders (in the users home folder under ~/mail), and what file we determine as the main INBOX (where mail is delivered to)
  3. Setup how we want to authenticate, and include the standard “passwordfile” auth configuration
  4. Make it so that we can have multiple connections to the server from the same IP Address

Kickoff

I always like to make sure I’ve got a message waiting for me, so I’ll send a dumb email to myself:

$ echo "imapic" | sendmail imapic

..and i’ll start dovecot

$ sudo service dovecot start

and see if all is okay by talking directly to dovecot:

$ telnet localhost 143
Trying ::1...
Connected to localhost.
Escape character is '^]'.
* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE STARTTLS AUTH=PLAIN] Dovecot ready.
A login imapic derp
A OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS MULTIAPPEND UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS] Logged in
B select INBOX
B NO [SERVERBUG] Internal error occurred. Refer to server log for more information. [2018-05-05 17:14:59]
C logout
* BYE Logging out
C OK Logout completed.
Connection closed by foreign host.

Debugging

Ah, seems we have an error! It recommends to check error logs located at /var/log/maillog:

$ sudo cat /var/log/maillog
May  5 17:13:13 dovecot: master: Dovecot v2.0.9 starting up (core dumps disabled)
May  5 17:14:51 dovecot: imap-login: Login: user=, method=PLAIN, rip=::1, lip=::1, mpid=1609, secured
May  5 17:14:59 dovecot: imap(imapic): Error: chown(/home/imapic/mail/.imap/INBOX, -1, 12(mail)) failed: Operation not permitted (egid=501(imapic), group based on /var/mail/imapic)
May  5 17:14:59 dovecot: imap(imapic): Error: mkdir(/home/imapic/mail/.imap/INBOX) failed: Operation not permitted
May  5 17:15:09 dovecot: imap(imapic): Disconnected: Logged out bytes=24/435

Our mail file is owned by our user, but it needs to also be a member of the same group for dovecot to even think about reading it.

$ ll /var/mail/imapic
-rw-rw----. 1 imapic mail 599 May  5 17:10 /var/mail/imapic

So, lets fix that up:

$ sudo usermod -G mail imapic
$ id imapic
uid=501(imapic) gid=501(imapic) groups=501(imapic),12(mail)

and we try again:

$ telnet localhost 143
Trying ::1...
Connected to localhost.
Escape character is '^]'.
* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE STARTTLS AUTH=PLAIN] Dovecot ready.
A login imapic derp
A OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS MULTIAPPEND UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS] Logged in
B select INBOX
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted.
* 1 EXISTS
* 1 RECENT
* OK [UNSEEN 1] First unseen.
* OK [UIDVALIDITY 1525555276] UIDs valid
* OK [UIDNEXT 2] Predicted next UID
* OK [HIGHESTMODSEQ 1] Highest
B OK [READ-WRITE] Select completed.
C logout
* BYE Logging out
C OK Logout completed.
Connection closed by foreign host.

Perfect! We have access to our inbox and we have 1 mail waiting for us.

Sendmail Configuration

Sendmail already works like a charm. We just need to tell it about our “new domain”, and what to do with messages destined to it.

For all this, we’re gonna work as root and in /etc/mail

local-host-names

This file contains all of the alternate host names of the server (i.e. domain-name.com). Sendmail will not accept mail for a domain unless it is permitted to do so by the contents of this file.

With all that written, we’ll add our domains in this file.

virtusertable

This file is heavily documented, but it comes down to adding the rule that all mail to the domain goes to the system account that we created back in the dovecot days.

...
@domain.com imapic

If we wanted mail from derp@domain.com to go to a different system account and not be dropped into the imapic “public” box, we’d add that rule above the one we’d created.

sendmail.mc

This, by far, is just something that I don’t personally understand. Items would need to be commented or changed to allow specific actions:

Add all the following to the bottom of the file:

Authentication rules:

define(`confAUTH_OPTIONS', `A')dnl
define(`confAUTH_MECHANISMS', `LOGIN PLAIN')dnl
TRUST_AUTH_MECH(`LOGIN PLAIN')dnl

Allow connections through a primary and secondary port for sending email messages:

DAEMON_OPTIONS(`Port=25,Name=MTA')dnl
DAEMON_OPTIONS(`Port=587, Name=MSA, M=E')dnl

To save a headache later on, delete any lines in the sendmail.mc that contain any configurations above.

Once you’ve saved everything, compile:

# /etc/mail/make

Kickoff

As simple as starting/restarting the services:

# service sendmail start
Starting sendmail:                                         [  OK  ]
Starting sm-client:                                        [  OK  ]
# service saslauthd start
Starting saslauthd:                                        [  OK  ]

Also, you might want to check if postfix has 25. Stop that if necessary.

Testing

Once you’ve started everything necessary, test to make sure you can connect and authenticate with the server. It will use system username/passwords to login, so you can create a test user before telnetting and testing:

$ sudo adduser test
$ sudo passwd test
...
# telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 hostname ESMTP Sendmail 8.14.4/8.14.4; Sat, 5 May 2018 21:12:03 -0400
HELO derp
250 hostname Hello localhost [127.0.0.1], pleased to meet you
AUTH LOGIN dGVzdA==
334 UGFzc3dvcmQ6
dGVzdA==
235 2.0.0 OK Authenticated
QUIT
221 2.0.0 hostname closing connection
Connection closed by foreign host.

dGVzdA==, in base64, is “test”. It’s both the username and password for this specific test account.

Service Wrap-up

Finally, after all this testing and configuration, we need to make sure we don’t have to do much more maintanance in case of server restart:

$ sudo chkconfig --level 345 dovecot on
$ sudo chkconfig --level 345 saslauthd on

PHP Automation

Part of the magic of all this is to now use the power of scripting to automatically “move” all incoming email to specific mailboxes.

So, we’ll need to install some additional software:

$ yum install php php-imap

And, as the all-mighty root, we setup imap-mail-mover.php

<?php
/**
 * The whole purpose of this script is to perform the following:
 * 1) Open an IMAP connection to an INBOX
 * 2) Look through all the messages
 * 3) Grab all messages and look for the first "To: " header in each message
 * 4) If the person in the "To: " is in the allowed domain
 * - We grab the user
 * - We check to see if their is a mailbox for that user, and move the messge there
 * - We delete the message
 * 5) If the person is not in the allowed domain
 * - We move the message to a default folder
 */

function get_imap_folders($resource, $config)
{
     // Get a list of mailboxes
     $original_folders = imap_listmailbox($resource, "{" . $config['server'] . ":" . $config['port'] . "}", "*");
     // these come through as {server:port}mailbox, so we just clean them up a bit
     $new_folders = array();
     $to_remove = "{" . $config['server'] . ":" . $config['port'] . "}";
     $folders = str_replace($to_remove, "", $original_folders);
     return $folders;
}

$config = array(
     'server' => 'localhost',
     'port' => '143',
     'username' => 'redacted',
     'password' => 'redacted',
     'folder' => 'INBOX',
     'spam' => 'SPAM',
     'debug' => true,
);
$debug_message = "";

$res = imap_open("{" . $config['server'] . ":" . $config['port'] . "/service=imap/novalidate-cert" . "}" . $config['folder'], $config['username'], $config['password']);
if (!$res)
{
     if ($config['debug'])
     {
     $debug_message = "IMAP Stream Failure";
     }
     die($debug_message);
}

$folders = get_imap_folders($res, $config);

// Lets get all the mail messages in the $config['folder']
$mbox = imap_check($res);
$number_messages = $mbox->Nmsgs;
if ($number_messages == 0)
{
     if ($config['debug'])
     {
     $debug_message = "No Messages";
     }
     die($debug_message);
}
$range = "1:" . $number_messages;

// now, we'll get the messages
$messages = imap_fetch_overview($res, $range);
foreach ($messages as $msg)
{
     $msgno = $msg->msgno;
     $to = $msg->to;

     echo "Message: " . $msg->subject . "\n";
     if (strpos($to, "@"))
     {
          $array_to = explode("@", $to);
          $to = $array_to[0];
     }

     // do we need to create a folder to move this message into?
     $destination_mbox = "{" . $config['server'] . ":" . $config['port'] . "}" . $to;
     if (!in_array($to, $folders))
     {
          if (imap_createmailbox($res, $destination_mbox))
          {
               echo "> Created folder [$to]\n";
          }
          else
          {
               echo "> Failed to create folder [$to]\n";
          }
     }
     $folders = get_imap_folders($res, $config);
     if (imap_mail_move($res, $msgno, $to))
     {
          echo "+ Moved successfully\n";
     }
     else
     {
          echo "- Failed to move message\n";
     }
}
imap_expunge($res);
imap_close($res);
?>

and, finally, we set some cron job to execute it:

$ crontab -e
# Create folder based on incoming message
*/1 * * * * /usr/bin/php -f /root/imap-mail-mover.php 2>/dev/null 1>&2

Leave a Reply