import email.message import mailbox import pathlib import shutil import socket import subprocess import textwrap import time import os import pytest def pytest_report_header(): which = shutil.which('notmuch') vers = subprocess.run(['notmuch', '--version'], stdout=subprocess.PIPE) return ['{} ({})'.format(vers.stdout.decode(errors='replace').strip(),which)] @pytest.fixture(scope='function') def tmppath(tmpdir): """The tmpdir fixture wrapped in pathlib.Path.""" return pathlib.Path(str(tmpdir)) @pytest.fixture def notmuch(maildir): """Return a function which runs notmuch commands on our test maildir. This uses the notmuch-config file created by the ``maildir`` fixture. """ def run(*args): """Run a notmuch command. This function runs with a timeout error as many notmuch commands may block if multiple processes are trying to open the database in write-mode. It is all too easy to accidentally do this in the unittests. """ cfg_fname = maildir.path / 'notmuch-config' cmd = ['notmuch'] + list(args) env = os.environ.copy() env['NOTMUCH_CONFIG'] = str(cfg_fname) proc = subprocess.run(cmd, timeout=120, env=env) proc.check_returncode() return run @pytest.fixture def maildir(tmppath): """A basic test interface to a valid maildir directory. This creates a valid maildir and provides a simple mechanism to deliver test emails to it. It also writes a notmuch-config file in the top of the maildir. """ cur = tmppath / 'cur' cur.mkdir() new = tmppath / 'new' new.mkdir() tmp = tmppath / 'tmp' tmp.mkdir() cfg_fname = tmppath/'notmuch-config' with cfg_fname.open('w') as fp: fp.write(textwrap.dedent("""\ [database] path={tmppath!s} [user] name=Some Hacker primary_email=dst@example.com [new] tags=unread;inbox; ignore= [search] exclude_tags=deleted;spam; [maildir] synchronize_flags=true """.format(tmppath=tmppath))) return MailDir(tmppath) class MailDir: """An interface around a correct maildir.""" def __init__(self, path): self._path = pathlib.Path(path) self.mailbox = mailbox.Maildir(str(path)) self._idcount = 0 @property def path(self): """The pathname of the maildir.""" return self._path def _next_msgid(self): """Return a new unique message ID.""" msgid = '{}@{}'.format(self._idcount, socket.getfqdn()) self._idcount += 1 return msgid def deliver(self, subject='Test mail', body='This is a test mail', to='dst@example.com', frm='src@example.com', headers=None, new=False, # Move to new dir or cur dir? keywords=None, # List of keywords or labels seen=False, # Seen flag (cur dir only) replied=False, # Replied flag (cur dir only) flagged=False): # Flagged flag (cur dir only) """Deliver a new mail message in the mbox. This does only adds the message to maildir, does not insert it into the notmuch database. :returns: A tuple of (msgid, pathname). """ msgid = self._next_msgid() when = time.time() msg = email.message.EmailMessage() msg.add_header('Received', 'by MailDir; {}'.format(time.ctime(when))) msg.add_header('Message-ID', '<{}>'.format(msgid)) msg.add_header('Date', time.ctime(when)) msg.add_header('From', frm) msg.add_header('To', to) msg.add_header('Subject', subject) if headers: for h, v in headers: msg.add_header(h, v) msg.set_content(body) mdmsg = mailbox.MaildirMessage(msg) if not new: mdmsg.set_subdir('cur') if flagged: mdmsg.add_flag('F') if replied: mdmsg.add_flag('R') if seen: mdmsg.add_flag('S') boxid = self.mailbox.add(mdmsg) basename = boxid if mdmsg.get_info(): basename += mailbox.Maildir.colon + mdmsg.get_info() msgpath = self.path / mdmsg.get_subdir() / basename return (msgid, msgpath)