Giter VIP home page Giter VIP logo

dodo's Introduction

Dodo

Documentation Status

Dodo is a graphical email client written in Python/PyQt6, based on the command line email swiss-army-knife notmuch.

Dodo

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 <[email protected]>'
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.

import dodo

dodo.settings.smtp_accounts = ['work', 'fun']

dodo.settings.email_address = {'work': 'First Last <[email protected]>',
                               'fun': 'First Last <[email protected]>'}
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 and a Python callback function to CommandBar.open. Here's an example which creates a new mode called 'notmuch' for running arbitrary notmuch commands:

import dodo
import subprocess

def run_notmuch(app):
    def callback(cmd):
        subprocess.run('notmuch ' + cmd, shell=True)
        app.refresh_panels()
    app.command_bar.open('notmuch', callback)

dodo.keymap.global_keymap['N'] = ('run notmuch from command bar', run_notmuch)

Custom layouts

You can customise the layout of the thread view by replacing the method ThreadPanel.layout_panel with your own version in config.py. This method is responsible for displaying three widgets: self.thread_list, self.message_info, and self.message_view. Normally, it draws the first two side-by-side, then places this above the third with an adjustable splitter.

Here's an example, making thread list twice as wide and putting the message view on top instead:

def my_layout(self):
    splitter = QSplitter(Qt.Orientation.Vertical)
    info_area = QWidget()
    info_area.setLayout(QHBoxLayout())
    self.thread_list.setFixedWidth(500)
    info_area.layout().addWidget(self.thread_list)
    info_area.layout().addWidget(self.message_info)
    splitter.addWidget(self.message_view)
    splitter.addWidget(info_area)
    self.layout().addWidget(splitter)

    # save splitter position
    window_settings = QSettings("dodo", "dodo")
    state = window_settings.value("thread_splitter_state")
    splitter.splitterMoved.connect(
            lambda x: window_settings.setValue("thread_splitter_state", splitter.saveState()))
    if state: splitter.restoreState(state)

dodo.thread.ThreadPanel.layout_panel = my_layout

Note that everything in PyQt6.QtCore and PyQt6.QtWidgets is already imported before config.py is exec'ed.

Snooze

Snoozing lets you temporarily hide messages to help clear your inbox (and your mind) for a few days at a time. After the snooze is up, they pop back into the inbox as unread messages again. Using notmuch hooks and Dodo, it is easy to set up some basic snooze functionality. Here's how I do it.

The basic idea is to tag messages with zzz- plus the date you want to see them again, then archive them. To make sure they pop back into the inbox on the correct date, add the following ~/MAILDIR/.notmuch/hooks/pre-new:

#!/bin/bash

notmuch tag -zzz-`date -I` +inbox +unread -- tag:zzz-`date -I`

This will automatically un-snooze messages tagged zzz-CURRENT-DATE whenever you refresh your email. Now, all you need to do is set up some keyboard shortcuts for snoozing messages. You can do this by adding the following to your config.py:

def snooze(days, mode='tag'):
    import datetime
    d = datetime.date.today() + datetime.timedelta(days=days)
    def f(search):
        search.tag_thread(f'-inbox -unread +zzz-{d}', mode)
    return f

dodo.keymap.search_keymap['z z'] = ("snooze for 1 day", snooze(days=1))
dodo.keymap.search_keymap['z w'] = ("snooze for 1 week", snooze(days=7))
dodo.keymap.search_keymap['z Z'] = ("snooze marked for 1 day", snooze(days=1, mode='tag marked'))
dodo.keymap.search_keymap['z W'] = ("snooze marked for 1 week", snooze(days=7, mode='tag marked'))

This allows snoozing single messages or bulk snoozing all marked messages (by default, you can (un)mark messages with <space>).

Note this doesn't return messages to the top, since it doesn't change the date they were received. Hence, it will work best if you keep your inbox relatively empty.

Setting up the prerequisites

Since there's a lot of little bits to configure, I've also included some minimal configurations for offlineimap, msmtp, and notmuch, just to have it all in one place.

Note the offlineimap and msmtp configurations below simply read the password from a plaintext file. More secure options are available, which are explained in the respective docs.

Incoming mail

Assuming your system configuration directory is ~/.config/, the configuration file for offlineimap is located in ~/.config/offlineimap/config. Here is a template for syncing one IMAP account named "Work":

[general]
accounts = Work

[Account Work]
localrepository = WorkLocal
remoterepository = WorkRemote

[Repository WorkLocal]
type = Maildir
localfolders = ~/mail/Work

[Repository WorkRemote]
type = IMAP
remotehost = (IMAP SERVER)
remoteuser = (USER NAME)
remotepassfile = (PASSWORD FILE)
sslcacertfile = OS-DEFAULT

If you want to set up multiple IMAP accounts, just put them all in the ~/mail folder and set ~/mail as your database path for notmuch.

Outgoing mail

Here is a sample ~/.config/msmtp/config, setting up a single SMTP server (also named "Work") with TLS:

defaults
auth           on
tls            on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile        ~/.msmtp.log
account        Work
host           (SMTP SERVER)
port           587
from           (EMAIL ADRESS)
user           (USER NAME)
passwordeval   cat (PASSWORD FILE)
account        default : Work

You may need to change the 4th line if your system stores CA certificates in a different location.

Mail indexing

Once offlineimap is set up, just run notmuch from the command line to do some initial setup, which gets saved in ~/.notmuch-config by default. You can set ~/mail as your database path. notmuch has lots of options, the ability to set up various hooks and filters, and to sync certain IMAP markers with notmuch tags.

Here's a ~/.notmuch-config which is roughly like the one I use:

[database]
path=/home/user/mail

[user]
name=First Last
[email protected]

[new]
tags=new
ignore=

[search]
exclude_tags=deleted;killed;spam;

[maildir]
synchronize_flags=true

Initial PGP/MIME support

Signing

The thread view panel will show the signature status of pgp-signed messages as reported by notmuch. notmuch tags them with te automatic tag signed.

Outgoing mail can be signed by setting dodo.settings.gnupg_keyid to the id of the key which will be used to do the signing. It can be disabled/enabled on a per-message basis in the comopose view by pressing the 's' key (or the key that is mapped to the toggle_pgp_sign function).

You might also have to set dodo.settings.gnupg_home.

Encryption

Encrypted mail has the encrypted automatic tag set by notmuch. The latter is also responsible for the decryption. (--decrypt=True option of notmuch show)

Outging mail can be encrypted by toggling the PGPEncrypt flag in the compose panel (mapped to the 'e' key). It is encrypted using the public keys found in the GnuPG database of all reciepients. Recepients without a public key in the GnuPG dataabse will not be able to decrypt the message.

More Screenshots

Searching:

Search

Thread view:

Thread

Composition:

Compose

Various themes:

Nord Solarized Dark Solarized Light

dodo's People

Contributors

agenbite avatar akissinger avatar cinghiopinghio avatar hbog avatar laarmen avatar leromarinvit avatar the-compiler avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

dodo's Issues

Core dump on big search

I hit / and searched for tag:delete (all my deleted mail), and dodo froze up for a few seconds before core dumping

newline character in the attachment filename, due to a folded content-disposition header, prevents forwarding mail.

For example, forwarding a mail message containing an attachment with a folded content-disposition header:

...
Content-Disposition: attachment;
        filename="Foo Bar Technology - Data Centre Hardware Maintenance and
 Monitoring (December 2021).pdf"; size=599285;
        creation-date="Thu, 04 Aug 2022 14:17:36 GMT";
        modification-date="Thu, 04 Aug 2022 14:21:02 GMT"
Content-Transfer-Encoding: base64
...

Results in the following attachment file

$ ls /tmp/dodo-n0njogin
'Foo Bar Technology - Data Centre Hardware Maintenance and'$'\n'' Monitoring (December 2021).pdf'

and the following mail composition template;

From: [email protected]
To:[email protected]
Subject: FW: Hardware Maintenance manual
A: Foo Bar Technology - Data Centre Hardware Maintenance and
 Monitoring (December 2021).pdf

Upon sending this forwarded mail, the exception Header values may not contain linefeed or carriage return characters is thrown

The presence of newline characters in the filename of the attachments may also have (security) implications with certain file management tools and scripts.

The issue is due to a bug in the get_filename() method of the (legacy) email.message.Message class. This bug is not present in the EmailMessage class:

$ python
>>> import email
>>> import email.policy
>>> f = open('mail.txt','r')
>>> msg = email.message_from_file(f)
>>> for part in msg.walk():
...   print(part.get_filename())
...
None
None
None
None
Foo Bar Technology - Data Centre Hardware Maintenance and
 Monitoring (December 2021).pdf
>>> f = open('mail.txt','r')
>>> msg = email.message_from_file(f,policy=email.policy.default)
>>> for part in msg.walk():
...   print(part.get_filename())
...
None
None
None
None
Foo Bar Technology - Data Centre Hardware Maintenance and Monitoring (December 2021).pdf

Caching message counts in tag view

This is the notmuch client I’ve been looking for!

I’ve noticed that with a very large (100k or more messages) database of archived email, re-querying message counts when the tag view is loaded has a significant delay. Might be nice to cache message counts and/or make the tallying a user-initiated process.

IndexError when viewing inexistent message

When viewing a message that's in the mail directory, but (I think?) not yet indexed by notmuch new, dodo crashes with:

Error opening /home/florian/mail/news/new/1685093528_0.355627.aragog,U=453271,FMD5=508c75c8507a2ae5223dfd2faeb98122:2,: No such file or directory
Error opening /home/florian/mail/news/new/1685094428_0.359005.aragog,U=453273,FMD5=508c75c8507a2ae5223dfd2faeb98122:2,: No such file or directory
Traceback (most recent call last):
  File "/home/florian/proj/dodo/dodo/mainwindow.py", line 63, in panel_focused
    if w.dirty: w.refresh()
                ^^^^^^^^^^^
  File "/home/florian/proj/dodo/dodo/thread.py", line 358, in refresh
    m = self.model.message_at(self.current_message)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/florian/proj/dodo/dodo/thread.py", line 201, in message_at
    return self.message_list.get(i, {})
           ^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'list' object has no attribute 'get'

due to this rather interesting output:

$ notmuch show --format=json --verify --include-html 0000000000052d67
Error opening /home/florian/mail/news/new/1685093528_0.355627.aragog,U=453271,FMD5=508c75c8507a2ae5223dfd2faeb98122:2,: No such file or directory
Error opening /home/florian/mail/news/new/1685094428_0.359005.aragog,U=453273,FMD5=508c75c8507a2ae5223dfd2faeb98122:2,: No such file or directory
[[[[[[]]]]]]

which results in self.message_list to be [] after flattening.

Using a default when getting the message instead:

diff --git i/dodo/thread.py w/dodo/thread.py
index f7d2be9..f4655f4 100644
--- i/dodo/thread.py
+++ w/dodo/thread.py
@@ -198,8 +198,11 @@ def refresh(self) -> None:
 
     def message_at(self, i: int) -> dict:
         """A JSON object describing the i-th message in the (flattened) thread"""
-
-        return self.message_list[i]
+        try:
+            return self.message_list[i]
+        except IndexError:
+            # notmuch failed
+            return {}
 
     def default_message(self) -> int:
         """Return the index of either the oldest unread message or the last message

results in dodo displaying an empty message:

image

though ideally I suppose this would maybe display some sort of error message, or even the stderr of the notmuch call.

address headers with non-ascii characters are not correctly saved and indexed

dodo correctly sends mail with non-ascii characters in the address headers (From, To, Cc).
However, when a new message is saved after sending, the address headers are not encoded correctly. The complete
header is encoded as one encoded word without separating the tokens.

For example the header
To: émail <é[email protected]>, mail <[email protected]>\n\n
is stored as
=?utf-8?b?w6ltYWlsIDzDqW1haWxAZXhhbXBsZS5uZXQ+LCBtYWlsIDxtYWlsQGV4YW1wbGUubmV0Pg==?=\n\n
in stead of
To: =?utf-8?q?=C3=A9mail?= <[email protected]>, mail <[email protected]>\n\n

notmuch is not able to decode the To address:

"headers": {
  ...     
  "To": "=?utf-8?b?w6ltYWlsIDzDqW1haWxAZXhhbXBsZS5uZXQ+LCBtYWlsIDxtYWlsQGV4YW1wbGUubmV0Pg==?=",
  ...
}

and hence the addresses are not indexed correctly. (For example, the message will not be found with a notmuch search to:example.net)

The root cause is most probably located in the parser of the email module which is used by the mailbox module.

Configuring panel layout

Is the panel layout easily configurable? I'd like to change the ThreadView panel sizes and positions

FR: change font in config file

Another thing that would be nice is to be able to define a font which can be used globally: currently, by setting dodo.settings.search_font we don't affect the css.

[feature request] dodo over ssh

Hello,

I am used to the following notmuch setup: all my emails are stored on a remote server and I use neomutt to access it throught ssh. I have also a local notmuch wrapper script to allow for some local operations (as described somewhere on the notmuch pages).

As far I see, this setup is mostly transparent for dodo since it seems to simply use whatever notmuch executable which is found in the PATH. However, this obviously does not work for attachments and for any operation needing to access the raw file. Can I do something in my config.py in the current state of dodo ? And if not, what would be the best way to deal with it ?

I imagine two different paths:

  • mount the remote maildir hierarchy over sshfs (but it needs to deal with the absolute path used in the xapian index)
  • retrieve each indivudal file when needed with scp

In both cases, we need some possibility to overload the Python code used to open files and I do not seen an obvious way to do that cleanly.

What do you think ?

Configuring vim in xfce4-terminal as editor

Depending on the terminal specification, the current definition scheme for the editor can fail. I'm trying to use the following in my config file:

dodo.settings.editor_command = ["xfce4-terminal", "--disable-server", "--geometry", "86x24+40+60", "-e", "vim"]

and I get an error xfce4-terminal: Unknown option "/tmp/tmp9ztrlszu.eml". In qutebrowser, for example, I can add {file} after "vim" and thus the call works.

FR: Forward message

Congrats on this interesting software which might indeed fill a gap. I gave it a try and missed the forwarding messages functionality. It'd be nice to have the possibility to forward an html-only message as a plain text message, something that I've found difficult in notmuch-emacs.

Keep it up!!

FR: Use ranger to pick attachments

Ranger's option --choosefiles could allow the user to pick the attachments without using the mouse. Is it possible to configure the file picker in the same way as the attachment viewer is configured?

Problem with mbsync

I've set dodo.settings.sync_mail_command = 'mbsync my-mail-box' and I'm getting this error at regular intervals:

FileNotFoundError: [Errno 2] No such file or directory: 'mbsync my-mail-box'

However, I have a system service for fetching mail through IMAP IDLE and I don't need Dodo to take care of fetching mail. Thus, I've set sync_mail_command to '' and the error is now

PermissionError: [Errno 13] Permission denied: ''

Adding an attachment removes the line feed between headers and body.

Attachments create a A: pseudo header, but this removes the (RFC defined) newline between headers and body. This has no impact on sending the mail because the python email package handles the absence of the newline silently (MissingHeaderBodySeparatorDefect). However when adding multiple attachments the A:-pseudo header will be added after the first empty line, which will now be in the body of the message.

For example

  • Before adding the attachment
From:
To:
Subject: 

Dear Addressee

Message ...
  • After adding first attachment, the newline between headers and body is removed
From:
To:
Subject: subject
A: attachment_1
Dear Addressee

Message ...
  • The second attachment pseudo header lands in the body
From:
To:
Subject: subject
A: attachment_1
Dear Addressee
A: attachment_2
Message ...

tags should be wrapped in double quotes in queries for tags with spaces

hey I'm using lieer which syncs tags to gmail labels so I'll have tags that looks like this "the tag/with stuff"

In dodo it lists all the tags but says there are zero messages for them.

Investigating a bit with notmuch, the following notmuch commands return nothing:

notmuch search tag:the tag/with stuff
notmuch search tag:"the tag/with stuff"

But the following does work:

notmuch search 'tag:"the tag/with stuff"'

I found that working combo here https://notmuchmail.org/pipermail/notmuch/2012/013532.html

so checking some of the calls dodo makes

stuff like this

r1 = subprocess.run(['notmuch', 'count', '--output=threads', '--', 'tag:'+t],

I think should be like

r1 = subprocess.run(['notmuch', 'count', '--output=threads', '--',  'tag:"%s"' % t],

just wrapping the tag in double quote (I think the extra single quotes are only needed in a shell)

I'll see if I can try fixing it

Use notmuch's python bindings

While investigating how to change the separator of the authors list in the search view (which is | [bar+space] and should be , [comma+space] or something similar), I noticed that Dodo makes calls to notmuch through subprocess.

Wouldn't it be better to use notmuch's python bindings instead of calls to subprocess.run?

Scroll the help window

Glad to see this keeps growing! :)

I find myself hitting 'j' in order to browse the help window containing the bindings. Would it be possible to customize bindings in this window too?

[feature request] reload config.

It would be cool to be able to reload the configuration (especially in an automated way or with a remote command), in particular this is my use case:

I managed to start dodo with a theme that match the current wm theme. At some point i want to change to dark or light theme. As of now I need to first change theme and then close and reopen dodo to make it follow the changes.

In astroid there was not such issue since, using gtk, it was automagically following the gtk theme.

Tags pane freezes UI for 50 seconds

For my setup, on a reasonably modern machine (Thinkpad T14 AMD), pressing T causes the UI to freeze for >50 seconds before displaying the list of 10 tags.

This mostly seems to be because I have an archive tag applied to a big number of messages which I imported into notmuch from an old (and chaotic) email setup. This, together with new and unread (which I might want to remove/rename for those archived messages, but that's a different story) causes notmuch count --output=threads -- "tag:... to run for around 15 seconds for all tags:

archive
212230
notmuch count --output=threads -- "tag:$line"  7.35s user 0.30s system 99% cpu 7.677 total

attachment
5487
notmuch count --output=threads -- "tag:$line"  0.19s user 0.03s system 99% cpu 0.217 total

draft
14
notmuch count --output=threads -- "tag:$line"  0.01s user 0.00s system 92% cpu 0.007 total

encrypted
851
notmuch count --output=threads -- "tag:$line"  0.06s user 0.00s system 98% cpu 0.063 total

flagged
9722
notmuch count --output=threads -- "tag:$line"  0.29s user 0.03s system 97% cpu 0.327 total

inbox
77
notmuch count --output=threads -- "tag:$line"  0.01s user 0.00s system 95% cpu 0.016 total

new
111838
notmuch count --output=threads -- "tag:$line"  3.70s user 0.19s system 99% cpu 3.918 total

replied
1919
notmuch count --output=threads -- "tag:$line"  0.06s user 0.02s system 98% cpu 0.077 total

signed
16132
notmuch count --output=threads -- "tag:$line"  0.53s user 0.03s system 99% cpu 0.556 total

unread
137843
notmuch count --output=threads -- "tag:$line"  3.77s user 0.19s system 99% cpu 3.970 total

That problem is then compounded by the same operation being run again with AND tag:unread (taking around 9 seconds), and by dodo apparently calling TagModel.refresh() twice in a row - once when initializing TagModel:

  /home/florian/proj/dodo/.venv/bin/dodo(8)<module>()
-> sys.exit(main())
  /home/florian/proj/dodo/dodo/app.py(292)main()
-> dodo.exec()
  /home/florian/proj/dodo/dodo/panel.py(150)keyPressEvent()
-> keymap.global_keymap[cmd][1](self.app)
  /home/florian/proj/dodo/dodo/keymap.py(31)<lambda>()
-> 'T':       ('show tags', lambda a: a.open_tags()),
  /home/florian/proj/dodo/dodo/app.py(221)open_tags()
-> p = tag.TagPanel(self, keep_open)
  /home/florian/proj/dodo/dodo/tag.py(145)__init__()
-> self.model = TagModel()
  /home/florian/proj/dodo/dodo/tag.py(41)__init__()
-> self.refresh()
> /home/florian/proj/dodo/dodo/tag.py(46)refresh()
-> self.beginResetModel()

and then again because .refresh() is immediately called on the TagPanel again when it is focused initially:

  /home/florian/proj/dodo/.venv/bin/dodo(8)<module>()
-> sys.exit(main())
  /home/florian/proj/dodo/dodo/app.py(292)main()
-> dodo.exec()
  /home/florian/proj/dodo/dodo/panel.py(150)keyPressEvent()
-> keymap.global_keymap[cmd][1](self.app)
  /home/florian/proj/dodo/dodo/keymap.py(31)<lambda>()
-> 'T':       ('show tags', lambda a: a.open_tags()),
  /home/florian/proj/dodo/dodo/app.py(222)open_tags()
-> self.add_panel(p)
  /home/florian/proj/dodo/dodo/app.py(129)add_panel()
-> self.tabs.setCurrentWidget(p)
  /home/florian/proj/dodo/dodo/mainwindow.py(63)panel_focused()
-> if w.dirty: w.refresh()
  /home/florian/proj/dodo/dodo/tag.py(161)refresh()
-> self.model.refresh()
> /home/florian/proj/dodo/dodo/tag.py(46)refresh()
-> self.beginResetModel()

I feel like the second issue should be solved either way (though I'm not sure what the best way to do that would be) - but probably for my setup this would still be too much of a delay. I played around with adding a timeout:

diff --git i/dodo/tag.py w/dodo/tag.py
index d1d01d7..3fa895b 100644
--- i/dodo/tag.py
+++ w/dodo/tag.py
@@ -50,12 +50,17 @@ def refresh(self) -> None:
         self.d: List[Tuple[str,str,str]] = []
 
         for t in tag_str.splitlines():
-            r1 = subprocess.run(['notmuch', 'count', '--output=threads', '--', 'tag:'+t],
-                    stdout=subprocess.PIPE)
-            c = r1.stdout.decode('utf-8').strip()
-            r1 = subprocess.run(['notmuch', 'count', '--output=threads', '--', f'tag:{t} AND tag:unread'],
-                    stdout=subprocess.PIPE)
-            cu = r1.stdout.decode('utf-8').strip()
+            try:
+                r1 = subprocess.run(['notmuch', 'count', '--output=threads', '--', 'tag:'+t],
+                        stdout=subprocess.PIPE, timeout=0.5)
+                c = r1.stdout.decode('utf-8').strip()
+                r1 = subprocess.run(['notmuch', 'count', '--output=threads', '--', f'tag:{t} AND tag:unread'],
+                        stdout=subprocess.PIPE, timeout=0.5)
+                cu = r1.stdout.decode('utf-8').strip()
+            except subprocess.TimeoutExpired:
+                c = "lots"
+                cu = "lots"
+
             self.d.append((t, cu, c))
 
         self.endResetModel()

which for me results in:

image

but, sadly, still takes around 6s to load (so assuming 3s with the double-refresh issue fixed, which still seems like a lot).


I suppose ideally the QAbstractItemModel would load this information lazily and use a QProcess to run notmuch instead, so that this doesn't block the UI. Then it would show up immediately, and update the UI as needed. But from my experience wrangling with QAbstractItemModel, it can be somewhat of a pain to do more advanced things like that...


edit: whoops, also see #16 - sorry for the duplicate! Leaving this open for now since it explores a couple of different approaches.

Confusing error message when `notmuch` setup is not completed

I tried to run dodo without having notmuch set up properly yet:

$ notmuch search
Error: cannot load config file.

doing so results in a rather confusing error:

$ dodo
Error: cannot load config file.
Error: cannot load config file.
Traceback (most recent call last):
  File "/home/florian/tmp/dodo/.venv/bin/dodo", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/florian/tmp/dodo/dodo/app.py", line 291, in main
    dodo = Dodo()
           ^^^^^^
  File "/home/florian/tmp/dodo/dodo/app.py", line 107, in __init__
    self.open_search(query, keep_open=True)
  File "/home/florian/tmp/dodo/dodo/app.py", line 179, in open_search
    p = search.SearchPanel(self, query, keep_open=keep_open)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/florian/tmp/dodo/dodo/search.py", line 170, in __init__
    self.model = SearchModel(q)
                 ^^^^^^^^^^^^^^
  File "/home/florian/tmp/dodo/dodo/search.py", line 42, in __init__
    self.refresh()
  File "/home/florian/tmp/dodo/dodo/search.py", line 50, in refresh
    self.d = json.loads(self.json_str)
             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

dodo crashes when trying to display a message with trash tag

When trying to display a message with a tag that is excluded in notmuch search results if it does not appear explicitly in the search terms, for example the trash tag, dodo aborts with the error IndexError: list index out of range.

Searching and displaying messages with such tags is a common use case, for example when one wants to remove a trash label that was accidentally applied.

Define custom bindings in config file

It is somehow inconvenient to customize key bindings in the source code tree, since any git pull will complain of conflicting changes and force the user to stash/merge. Wouldn't it be nicer to edit the bindings directly in the config.py file? I tried to copy the mapping dictionaries into that file and it doesn't seem to work.

(Sorry for too many feature requests and too few PRs, but python is not my mother tongue... Thanks again for this software!!)

[feature request] multiple accounts

Thanks for this sleek MUA.
Nowadays, many of us use multiple email accounts for different channel of communication.
Is it a planned feature to support multiple accounts?
As of now, it seams only one account is configurable.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.