Dodo
A graphical, hackable email client based on notmuch
Install / Use
/learn @akissinger/DodoREADME
Dodo
Dodo is a graphical email client written in Python/PyQt6, based on the command line email swiss-army-knife notmuch.
![]()
It's main goals are to:
- offer efficient, keyboard-oriented mail reading, sorting, and composing
- give a mostly text-based email experience by default, but with HTML support a few keystrokes away
- offload as much work as possible on existing, excellent command-line tools (UNIX philosphy-style)
- be simple enough to customise and hack on yourself
This README has instructions on installation, usage, and basic configuration. For API documentation (which is also useful for configuration), check out the Read the Docs page.
As an email client, Dodo is pretty much feature-complete, but not yet extensively tested. Since it's based on notmuch, all of its features are non-destructive, so you shouldn't ever lose any email due to bugs. That being said, you might see some strange behaviour, so use at your own risk.
A lot of Dodo's design is inspired by two existing notmuch-based clients: alot and astroid.
Prerequisites
If you have already used notmuch for email, there's not much to do here :). If not, you'll need to set up some other programs first:
- something to check mail and sync with a local Maildir (offlineimap is the default, but others like mbsync should work fine)
- a sendmail-compatible SMTP client to send mail (msmtp is the default)
- notmuch for email searching and tagging
- w3m for translating HTML messages into plaintext
- python-gnupg for pgp/mime support (optional)
All of this is pretty standard stuff, and should be installable via your package manager on Linux/Mac/etc. If you don't know how to set these things up already, see the respective websites or the "Setting up the prerequisites" section below for a quick reference.
Install and run
Dodo requires Python 3.7+ and PyQt6 6.2 or above. You can install the latest git version of Dodo and its dependencies using pip:
git clone https://github.com/akissinger/dodo.git
cd dodo
pip install .
Then, run Dodo with:
dodo
If you don't have it already, you may need to add ~/.local/bin to your PATH.
Basic use
Before you fire up Dodo for the first time, make sure you at least configure email_address and sent_dir in config.py (see next section).
Most functionality in Dodo comes from keyboard shortcuts. Press ? to get a full list of the key mappings at any time.
Dodo has 4 different kinds of view: search views, thread views, compose views, and the tag view. It opens initially with a search view with the query tag:inbox. Pressing enter or double-clicking a thread with open that thread in the thread view. Pressing c at any time or r while looking at a message in the thread view will open the compose view. Pressing T will open a list of all the known tags in a new tab.
In the compose view, press <enter> to edit the message on your chosen editor. Once you save and exit, the message will be updated. Press a to add attachments (or use the special A: header). Press S to send.
Configuration
Dodo is configured via ~/.config/dodo/config.py. This is just a Python file that gets eval-ed right before the main window is shown.
Settings and their default values are defined in settings.py. A complete list, with documentation, can be found here.
Most settings have reasonable defaults (assuming your are using offlineimap/msmtp). The only two things that must be set for Dodo to work properly are your email address and the location of your sent mail folder. Some things you probably also want to set up are the text editor (for composing messages) and the file browser (for viewing attachments).
Here is an example config.py, with some settings similar to the ones I use:
import dodo
# required
dodo.settings.email_address = 'First Last <me@domain.com>'
dodo.settings.sent_dir = '/home/user/mail/Work/Sent'
# optional
dodo.settings.theme = dodo.themes.nord
dodo.settings.editor_command = "kitty nvim '{file}'"
dodo.settings.file_browser_command = "fman '{dir}' /home/user/Documents"
A theme is just a Python dictionary mapping some fixed color names to HTML color codes. Currently, the themes implemented in themes.py are catppuccin_macchiato, nord, solarized_light and solarized_dark. If you want more, feel free to roll your own, or (better) send me a pull request!
All of the settings of the form ..._command are given as shell command. The editor_command setting takes a placeholder {file} for the file to edit and file_browser_command takes the placeholder {dir} for the directory to browse.
The settings above replace the default text editor (xterm -e vim) with neovim run inside a new kitty terminal. I am also using Michael Herrmann's excellent dual-pane file manager fman instead of the default (nautilus). With these settings, showing attachments will open fman with a fixed directory in the right pane (/home/user/Documents) and a directory containing the attachments on the left. A similar effect can be obtained with ranger using the multipane view mode.
If you are using a file browser that supports it, you can also set a custom file_picker_command for choosing attachments. This setting is None by default, which tells Dodo to use the built-in file picker. This accepts a {tempfile} placeholder, where the names of the chosen files should be written after running the command. Here's an example using ranger --choosefiles:
dodo.settings.file_picker_command = "kitty ranger --choosefiles='{tempfile}'"
While Javascript is disabled in the HTML email viewer, you may want to set up a custom HTML sanitizer function as follows:
dodo.util.html2html = dodo.util.clean_html2html
The above function passes the HTML through the Cleaner object of the bleach library. Note this still allows some dodgy stuff, such as calling home via embedded img tags, so remote requests from HTML messages are disabled by default via the setting html_block_remote_requests. Javascript is also disabled.
Key mapping
Key mappings can be customised by changing the dictionaries defined in keymap.py. These map a key to a pair consisting of a description string and a Python function. For the global_keymap, this function takes the Dodo object defined in app.py as its argument. The other maps take the relevant "local" widget (SearchView, ThreadView, ComposeView, or CommandBar).
To bind a single key, you can write something like this in config.py:
dodo.keymap.search_keymap['t'] = (
'toggle todo',
lambda p: p.toggle_thread_tag('todo'))
or you can replace the keymap completely from config.py, e.g.:
dodo.keymap.search_keymap = {
'C-n': ('next thread', lambda p: p.next_thread()),
'C-p': ('previous thread', lambda p: p.previous_thread()),
# ...
}
The keymaps used by Dodo are global_keymap, search_keymap, thread_keymap, and command_bar_keymap. All the keymaps except command_bar_keymap also support keychords, which are represented as space-separated sequences of keypresses, e.g.
dodo.keymap.global_keymap['C-x C-c'] = (
'exit emacs ... erm, I mean Dodo',
lambda a: a.quit())
You can unmap a single key by deleting it from the dictionary:
del dodo.keymap.global_keymap['Q']
Multiple accounts
If you are using something like msmtp to send emails, it is possible to send mail from multiple accounts. To set this up, simply set a list of account names your SMTP client recognises in config.py. You can also provide per-account email addresses and sent directories by passing dictionaries to email_address and sent_dir settings, respectively. The gnupg_keyid setting can also be set to a dictionary mapping account names to GPG key IDs. If an account name is missing, signing is automatically disabled when switching to the account in question.
import dodo
dodo.settings.smtp_accounts = ['work', 'fun']
dodo.settings.email_address = {'work': 'First Last <me@super-serious-company.com>',
'fun': 'First Last <me@super-silly-domain.ninja>'}
dodo.settings.sent_dir = {'work': '/home/user/mail/Work/Sent',
'fun': '/home/user/mail/Fun/Sent'}
By default, you can use the [ and ] keys to cycle through different accounts in the Compose panel. The first account in the list is selected by default.
For multiple incoming mail accounts, just sync all accounts into subdirectories of a single directory and point notmuch to the main directory.
Custom commands with the command bar
By default, the command bar can be opened in two modes, 'search' and 'tag', for searching and tagging messages, respectively. You can create more modes on-the-fly from config.py by passing a new name an
