├── .davemailrc ├── .gitignore ├── .mbsyncrc ├── .msmtprc ├── .notmuch-config ├── .vdirsyncerrc ├── README.md ├── davemail.el ├── davemail.py ├── postsync.py ├── presync.py └── syncmail /.davemailrc: -------------------------------------------------------------------------------- 1 | [kzar] 2 | default_folder=Archive 3 | tag_new_messages=True 4 | maintain_tag_folder_mapping=True 5 | 6 | [[tag_folder_mapping]] 7 | inbox=INBOX 8 | travel=Travel 9 | spam=Spam 10 | deleted=Trash 11 | 12 | [[old_message_archival]] 13 | old_folder=Old 14 | date_since=6months 15 | 16 | [duckduckgo] 17 | default_folder=Archive 18 | tag_new_messages=True 19 | maintain_tag_folder_mapping=False 20 | 21 | [[tag_folder_mapping]] 22 | inbox=INBOX 23 | asana=Asana 24 | mattermost=Mattermost 25 | ft-privacyeng=ft-privacyeng 26 | spam="Junk Email" 27 | deleted="Deleted Items" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /.mbsyncrc: -------------------------------------------------------------------------------- 1 | # IMAP keeps an "internaldate" attribute for messages, which is separate 2 | # from the date given in the message header, set based upon when the 3 | # message was first received. Fastmail's webmail interface at least 4 | # uses this attribute to properly order messages chronologically. 5 | # The CopyArrivalDate option isn't well documented but it seems that when 6 | # synchronising a new message it uses the Maildir file's Modify date as the 7 | # IMAP internaldate attribute and vice versa. Otherwise it seemed the 8 | # synchronisation time was being used instead. By setting the option here it's 9 | # enabled by default for all Channels. 10 | CopyArrivalDate yes 11 | 12 | IMAPAccount kzar 13 | Host imap.fastmail.com 14 | SSLType IMAPS 15 | SSLVersions TLSv1.2 16 | User kzar@kzar.co.uk 17 | PassCmd "secret-tool lookup email kzar@kzar.co.uk" 18 | AuthMechs PLAIN 19 | 20 | IMAPStore kzar-remote 21 | Account kzar 22 | 23 | MaildirStore kzar-local 24 | # The trailing "/" is important 25 | Path ~/Maildir/kzar/ 26 | Inbox ~/Maildir/kzar/INBOX 27 | Flatten . 28 | 29 | Channel kzar 30 | Far :kzar-remote: 31 | Near :kzar-local: 32 | Patterns INBOX Travel Archive Sent Spam Trash 33 | Expunge Both 34 | SyncState * 35 | Sync All 36 | Create Both 37 | 38 | # IMAPAccount duckduckgo 39 | # Host outlook.office365.com 40 | # SSLType IMAPS 41 | # SSLVersions TLSv1.2 42 | # User dvandyke@duckduckgo.com 43 | # PassCmd "secret-tool lookup email dvandyke@duckduckgo.com" 44 | # 45 | # IMAPStore duckduckgo-remote 46 | # Account duckduckgo 47 | # 48 | # MaildirStore duckduckgo-local 49 | # # The trailing "/" is important 50 | # Path ~/Maildir/duckduckgo/ 51 | # Inbox ~/Maildir/duckduckgo/INBOX 52 | # Flatten . 53 | # 54 | # Channel duckduckgo 55 | # Far :duckduckgo-remote: 56 | # Near :duckduckgo-local: 57 | # Expunge Both 58 | # Patterns * 59 | # SyncState * 60 | # Sync All 61 | # Create Both -------------------------------------------------------------------------------- /.msmtprc: -------------------------------------------------------------------------------- 1 | defaults 2 | tls_trust_file /etc/ssl/certs/ca-certificates.crt 3 | auth on 4 | protocol smtp 5 | tls on 6 | tls_trust_file /etc/ssl/certs/ca-certificates.crt 7 | 8 | account kzar 9 | host smtp.fastmail.com 10 | port 465 11 | user kzar@kzar.co.uk 12 | passwordeval "echo `secret-tool lookup email kzar@kzar.co.uk`" 13 | tls_starttls off 14 | from kzar@kzar.co.uk 15 | 16 | # account duckduckgo 17 | # host smtp.office365.com 18 | # port 587 19 | # user dvandyke@duckduckgo.com 20 | # passwordeval "echo `secret-tool lookup email dvandyke@duckduckgo.com`" 21 | # from dvandyke@duckduckgo.com 22 | 23 | account default : kzar -------------------------------------------------------------------------------- /.notmuch-config: -------------------------------------------------------------------------------- 1 | # .notmuch-config - Configuration file for the notmuch mail system 2 | # 3 | # For more information about notmuch, see https://notmuchmail.org 4 | 5 | # Database configuration 6 | # 7 | # The only value supported here is 'path' which should be the top-level 8 | # directory where your mail currently exists and to where mail will be 9 | # delivered in the future. Files should be individual email messages. 10 | # Notmuch will store its database within a sub-directory of the path 11 | # configured here named ".notmuch". 12 | # 13 | [database] 14 | path=/home/kzar/Maildir 15 | 16 | # User configuration 17 | # 18 | # Here is where you can let notmuch know how you would like to be 19 | # addressed. Valid settings are 20 | # 21 | # name Your full name. 22 | # primary_email Your primary email address. 23 | # other_email A list (separated by ';') of other email addresses 24 | # at which you receive email. 25 | # 26 | # Notmuch will use the various email addresses configured here when 27 | # formatting replies. It will avoid including your own addresses in the 28 | # recipient list of replies, and will set the From address based on the 29 | # address to which the original email was addressed. 30 | # 31 | [user] 32 | name=Dave Vandyke 33 | primary_email=kzar@kzar.co.uk 34 | other_email=dave@suboptimal.co.uk;dave@vandyke.co.uk;zy661841@ou.ac.uk;dvandyke@duckduckgo.com;kzar@chromium.org 35 | 36 | # Configuration for "notmuch new" 37 | # 38 | # The following options are supported here: 39 | # 40 | # tags A list (separated by ';') of the tags that will be 41 | # added to all messages incorporated by "notmuch new". 42 | # 43 | # ignore A list (separated by ';') of file and directory names 44 | # that will not be searched for messages by "notmuch new". 45 | # 46 | # NOTE: *Every* file/directory that goes by one of those 47 | # names will be ignored, independent of its depth/location 48 | # in the mail store. 49 | # 50 | [new] 51 | tags=inbox;new 52 | ignore=.mbsyncstate;.isyncuidmap.db;.uidvalidity 53 | 54 | # Search configuration 55 | # 56 | # The following option is supported here: 57 | # 58 | # exclude_tags 59 | # A ;-separated list of tags that will be excluded from 60 | # search results by default. Using an excluded tag in a 61 | # query will override that exclusion. 62 | # 63 | [search] 64 | exclude_tags=deleted;spam;muted 65 | 66 | # Maildir compatibility configuration 67 | # 68 | # The following option is supported here: 69 | # 70 | # synchronize_flags Valid values are true and false. 71 | # 72 | # If true, then the following maildir flags (in message filenames) 73 | # will be synchronized with the corresponding notmuch tags: 74 | # 75 | # Flag Tag 76 | # ---- ------- 77 | # D draft 78 | # F flagged 79 | # P passed 80 | # R replied 81 | # S unread (added when 'S' flag is not present) 82 | # 83 | # The "notmuch new" command will notice flag changes in filenames 84 | # and update tags, while the "notmuch tag" and "notmuch restore" 85 | # commands will notice tag changes and update flags in filenames 86 | # 87 | [maildir] 88 | synchronize_flags=true 89 | 90 | # Cryptography related configuration 91 | # 92 | # The following option is supported here: 93 | # 94 | # gpg_path 95 | # binary name or full path to invoke gpg. 96 | # 97 | [crypto] 98 | gpg_path=gpg 99 | -------------------------------------------------------------------------------- /.vdirsyncerrc: -------------------------------------------------------------------------------- 1 | [general] 2 | status_path = "~/.vdirsyncer/status/" 3 | 4 | [pair card] 5 | a = "card_local" 6 | b = "card_remote" 7 | collections = ["from b", "from a"] 8 | 9 | [pair cal] 10 | a = "cal_local" 11 | b = "cal_remote" 12 | collections = ["from b", "from a"] 13 | 14 | [storage cal_local] 15 | type = "filesystem" 16 | path = "~/.calendar" 17 | fileext = ".vcf" 18 | 19 | [storage card_local] 20 | type = "filesystem" 21 | path = "~/.contacts" 22 | fileext = ".vcf" 23 | 24 | [storage cal_remote] 25 | type = "caldav" 26 | url = "https://caldav.fastmail.com/" 27 | username = "kzar@kzar.co.uk" 28 | password.fetch = ["command", "secret-tool", "lookup", "email", "kzar@kzar.co.uk"] 29 | 30 | [storage card_remote] 31 | type = "carddav" 32 | url = "https://carddav.fastmail.com/" 33 | username = "kzar@kzar.co.uk" 34 | password.fetch = ["command", "secret-tool", "lookup", "email", "kzar@kzar.co.uk"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Davemail 2 | 3 | ## Introduction 4 | 5 | My email setup, now under source control as it is getting rather complex! 6 | 7 | 0. My personal emails come into my [FastMail][1] account. 8 | 1. [mbsync][2] is used to keep a local copy of my emails synchronised using 9 | IMAP. (Much more efficient and reliably than [offlineimap][3] once set up 10 | correctly. Unfortunately setting it up correctly [wasn't trivial][4]). 11 | 2. My pre and post synchronisation scripts are run which take care of 12 | the tag to folder mapping as specified in .davemailrc. For example, 13 | a message with the tag `inbox` should be moved to the `INBOX` folder, and a 14 | message with no relevant tags should be moved to the `Archive` folder. 15 | 3. Those scripts also run my pre and post synchronisation hooks in Emacs, which 16 | I then use to update my modeline display etc. 17 | 4. [Notmuch][6] Emacs client is then used for reading and tagging message. 18 | [Gnus alias][7] for handling my different email identities + signatures. 19 | 5. Outgoing emails are sent using [msmtp][8], back out via FastMail's servers. 20 | 6. [vdirsyncer][9] synchronises my personal contacts and calendars with FastMail 21 | so that I have a local copy to use for address completion etc. 22 | 23 | ## Usage 24 | 25 | _(Not actually supposed to be used by other people...)_ 26 | 27 | 1. Install Emacs, Notmuch, msmtp, isync, gpg, libsecret-tools, vdirsyncer etc. 28 | 2. Install Python and configobj. 29 | 3. Set up [my Emacs config][5]. 30 | 4. Store email server passwords e.g. 31 | `secret-tool store --label="Email: kzar@kzar.co.uk" email kzar@kzar.co.uk` 32 | 5. Set up symlinks for the configuration files: 33 | ``` 34 | ln -s ~/path/to/davemail/.mbsyncrc ~/ 35 | ln -s ~/path/to/davemail/.msmtprc ~/ 36 | ln -s ~/path/to/davemail/.notmuch-config ~/ 37 | ln -s ~/path/to/davemail/.vdirsyncerrc ~/.config/vdirsyncer/config 38 | ``` 39 | 40 | - Run `./syncmail` to do a complete mail synchronisation every two minutes. 41 | - Press Enter to have the script synchronise again immediately. 42 | - Run `vdirsyncer sync` to synchronise personal contacts and calendar with 43 | Fastmail. 44 | 45 | ## TODO 46 | 47 | - `maintain_tag_folder_mapping` does not behave well when enabled for multiple 48 | accounts if an email is forwarded between the accounts. It is moved to the 49 | first account's Maildir, even if the mail should exist in both Maildirs. 50 | That, in turn, screws up the tags. 51 | - Figure out how to consider the address the original email was to when 52 | forwarding emails. Currently, the wrong alias is often used. 53 | - [Set up imapnotify to trigger mbsync, rather than polling.][10] 54 | - Sending emails using msmtp blocks Emacs, which sucks when the connection to 55 | Fastmail is slow. 56 | - Finish the old message archiving functionality? 57 | - Perhaps replace some of davemail.py with [imapfilter][11]? 58 | - Have notmuch use my local copy of my contacts [for address completion][12] as 59 | well as the notmuch database. (See `notmuch-address-command`.) 60 | - Use [syncmaildir][13] instead of IMAP? 61 | - Replace msmtp with [nullmailer][14]? 62 | - Figure out how to get Outlook SSO working for my work emails. 63 | 64 | [1]: https://fastmail.com 65 | [2]: http://isync.sourceforge.net/mbsync.html 66 | [3]: http://www.offlineimap.org 67 | [4]: http://isync.sourceforge.net/mbsync.html#INHERENT%20PROBLEMS 68 | [5]: https://github.com/kzar/emacs.d 69 | [6]: https://notmuchmail.org/ 70 | [7]: https://www.emacswiki.org/emacs/GnusAlias 71 | [8]: http://msmtp.sourceforge.net/ 72 | [9]: https://vdirsyncer.pimutils.org/en/stable/index.html 73 | [10]: https://martinralbrecht.wordpress.com/2016/05/30/handling-email-with-emacs/ 74 | [11]: https://raymii.org/s/blog/Filtering_IMAP_mail_with_imapfilter.html 75 | [12]: https://notmuchmail.org/emacstips/#index13h2 76 | [13]: http://syncmaildir.sourceforge.net/ 77 | [14]: http://www.troubleshooters.com/linux/nullmailer/ 78 | -------------------------------------------------------------------------------- /davemail.el: -------------------------------------------------------------------------------- 1 | (define-key global-map "\C-cm" 'notmuch) 2 | (setq message-kill-buffer-on-exit t 3 | notmuch-search-oldest-first t 4 | notmuch-fcc-dirs 5 | '(("dvandyke@duckduckgo.com" . 6 | "\"duckduckgo/Sent Items\" +sent +duckduckgo -inbox") 7 | (".*" . "\"kzar/Sent\" +sent +kzar -inbox")) 8 | notmuch-crypto-process-mime nil 9 | notmuch-saved-searches 10 | '((:name "inbox" :query "tag:inbox" :key "i") 11 | (:name "inbox work" :query "tag:inbox AND tag:suboptimal" :key "w") 12 | (:name "travel" :query "tag:travel" :key "t") 13 | ;(:name "unread" :query "tag:unread" :key "u") 14 | (:name "flagged" :query "tag:flagged" :key "f") 15 | (:name "all mail" :query "*" :key "d"))) 16 | (eval-after-load "notmuch" 17 | (lambda () 18 | (define-key notmuch-common-keymap "g" 'notmuch-refresh-this-buffer) 19 | (define-key 'notmuch-show-part-map "d" 'my-notmuch-show-view-as-patch))) 20 | 21 | ; Modline incoming message notifications 22 | (setq kzar/notmuch-activity-string "") 23 | (add-to-list 'global-mode-string '((:eval kzar/notmuch-activity-string)) t) 24 | (defun kzar/get-notmuch-incoming-count () 25 | (string-trim 26 | (shell-command-to-string 27 | "notmuch count tag:inbox AND tag:unread"))) 28 | (defun kzar/format-notmuch-mode-string (count) 29 | (let* ((no-email (string= count "0")) 30 | (email-icon (if no-email "📭" "📬"))) 31 | (concat email-icon "[" (if no-email "" count) "]"))) 32 | (defun kzar/update-notmuch-activity-string (&rest args) 33 | (setq kzar/notmuch-activity-string 34 | (kzar/format-notmuch-mode-string (kzar/get-notmuch-incoming-count))) 35 | (force-mode-line-update)) 36 | 37 | (add-hook 'notmuch-after-tag-hook 'kzar/update-notmuch-activity-string) 38 | (defcustom notmuch-presync-hook nil 39 | "Hook run before notmuch is synchronised" 40 | :type 'hook) 41 | (defcustom notmuch-postsync-hook '(kzar/update-notmuch-activity-string) 42 | "Hook run after notmuch has been synchronised" 43 | :type 'hook) 44 | 45 | (add-hook 'message-setup-hook 46 | (lambda () 47 | (gnus-alias-determine-identity) 48 | (define-key message-mode-map (kbd "C-c f") 49 | (lambda () 50 | (interactive) 51 | (message-remove-header "Fcc") 52 | (message-remove-header "Organization") 53 | (gnus-alias-select-identity) 54 | (notmuch-fcc-header-setup))) 55 | (flyspell-mode))) 56 | 57 | ; https://notmuchmail.org/emacstips/#index25h2 58 | (defun my-notmuch-show-view-as-patch () 59 | "View the the current message as a patch." 60 | (interactive) 61 | (let* ((id (notmuch-show-get-message-id)) 62 | (msg (notmuch-show-get-message-properties)) 63 | (part (notmuch-show-get-part-properties)) 64 | (subject (concat "Subject: " (notmuch-show-get-subject) "\n")) 65 | (diff-default-read-only t) 66 | (buf (get-buffer-create (concat "*notmuch-patch-" id "*"))) 67 | (map (make-sparse-keymap))) 68 | (define-key map "q" 'notmuch-bury-or-kill-this-buffer) 69 | (switch-to-buffer buf) 70 | (let ((inhibit-read-only t)) 71 | (erase-buffer) 72 | (insert subject) 73 | (insert (notmuch-get-bodypart-text msg part nil))) 74 | (set-buffer-modified-p nil) 75 | (diff-mode) 76 | (lexical-let ((new-ro-bind (cons 'buffer-read-only map))) 77 | (add-to-list 'minor-mode-overriding-map-alist new-ro-bind)) 78 | (goto-char (point-min)))) 79 | 80 | ; gnus-alias 81 | (autoload 'gnus-alias-determine-identity "gnus-alias" "" t) 82 | (setq gnus-alias-identity-alist 83 | '(("kzar" 84 | nil ;; Does not refer to any other identity 85 | "Dave Vandyke " 86 | nil ;; No organization header 87 | nil ;; No extra headers 88 | nil ;; No extra body text 89 | nil) 90 | ("duckduckgo" 91 | nil ;; Does not refer to any other identity 92 | "Dave Vandyke " 93 | "DuckDuckGo" 94 | nil ;; No extra headers 95 | nil ;; No extra body text 96 | nil))) 97 | 98 | (setq gnus-alias-default-identity "kzar") 99 | (setq gnus-alias-identity-rules 100 | '(("@duckduckgo.com" ("any" "@duckduckgo\\.com" both) "duckduckgo"))) 101 | 102 | ; Outgoing email (msmtp + msmtpq) 103 | (setq send-mail-function 'sendmail-send-it 104 | sendmail-program "/usr/bin/msmtp" 105 | mail-specify-envelope-from t 106 | message-sendmail-envelope-from 'header 107 | mail-envelope-from 'header) 108 | 109 | ; PGP encrypted email 110 | (setq notmuch-crypto-mime t 111 | notmuch-crypto-process-mime t 112 | mml-secure-openpgp-encrypt-to-self t) 113 | -------------------------------------------------------------------------------- /davemail.py: -------------------------------------------------------------------------------- 1 | import os 2 | from subprocess import call, check_output 3 | from configobj import ConfigObj 4 | import re 5 | import email 6 | 7 | import notmuch 8 | 9 | config = ConfigObj(".davemailrc") 10 | for maildir in config: 11 | # Prevent the new tag from being mapped to a folder. 12 | if "new" in config[maildir]["tag_folder_mapping"]: 13 | del config[maildir]["tag_folder_mapping"]["new"] 14 | 15 | def move_messages(query_string, maildir, destination_folder): 16 | with notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) as db: 17 | query = db.create_query(query_string) 18 | 19 | for message in query.search_messages(): 20 | old_filename = message.get_filename() 21 | path, filename = os.path.split(old_filename) 22 | cur_new = path.split(os.sep)[-1] 23 | # We strip the ,U=nnn infix from the filename so that mbsync isn't 24 | # confused when using the native storage scheme. 25 | # https://sourceforge.net/p/isync/mailman/message/33359742/ 26 | new_filename = os.path.join(db.get_path(), maildir, destination_folder, 27 | cur_new, re.sub(",U=[0-9]+", "", filename)) 28 | # If the old file no longer exists, or the new one already does then we 29 | # simply skip this message for now. 30 | try: 31 | os.renames(old_filename, new_filename) 32 | except OSError: 33 | continue 34 | # We add the new filename to the notmuch database before removing the old 35 | # one so that the notmuch tags are preserved for the message. 36 | db.add_message(new_filename) 37 | db.remove_message(old_filename) 38 | 39 | def move_tagged_messages(): 40 | for maildir in config: 41 | if config[maildir].as_bool("maintain_tag_folder_mapping"): 42 | default_folder = config[maildir]["default_folder"] 43 | for tag, folder in config[maildir]["tag_folder_mapping"].iteritems(): 44 | # First we move tagged messages into the folder. 45 | move_messages("tag:%s AND NOT folder:\"%s/%s\" AND path:%s/**" % 46 | (tag, maildir, folder, maildir), 47 | maildir, folder) 48 | # Then untagged messages out. 49 | move_messages("NOT tag:%s AND folder:\"%s/%s\"" % 50 | (tag, maildir, folder), 51 | maildir, default_folder) 52 | 53 | def tag_messages(query_string, tag): 54 | # We tag/untag the messages by calling the notmuch command here since 55 | # the Python notmuch API doesn't provide a way to tag messages without 56 | # iterating through them. 57 | call(["notmuch", "tag", tag, query_string]) 58 | 59 | def tag_moved_and_new_messages(): 60 | for maildir in config: 61 | # Tag messages based on their maildir 62 | tag_messages("path:%s/**" % (maildir), "+" + maildir) 63 | 64 | for tag, folder in config[maildir]["tag_folder_mapping"].iteritems(): 65 | if config[maildir].as_bool("maintain_tag_folder_mapping"): 66 | # Tag / untag all messages if maintaining tag to folder mapping 67 | tag_messages("NOT folder:\"%s/%s\" AND path:%s/**" % 68 | (maildir, folder, maildir), "-" + tag) 69 | tag_messages("folder:\"%s/%s\"" % 70 | (maildir, folder), "+" + tag) 71 | elif config[maildir].as_bool("tag_new_messages"): 72 | # Otherwise only tag new messages which were already filed e.g. spam 73 | tag_messages("folder:\"%s/%s\" AND tag:new" % 74 | (maildir, folder), "+" + tag) 75 | 76 | def tag_muted_threads(): 77 | # New messages in muted threads won't have the muted tag yet, so to avoid 78 | # those showing up in searches we take care to tag them now. 79 | # See https://notmuchmail.org/excluding/ 80 | muted_threads = check_output( 81 | ["notmuch", "search", "--output=threads", "tag:muted"] 82 | ).decode("utf-8").replace(os.linesep, " ").strip() 83 | tag_messages(muted_threads, "+muted") 84 | 85 | def header_matches(filename, header_name, regexp): 86 | with open(filename, "rb") as f: 87 | message = email.message_from_binary_file(f) 88 | 89 | for header_value in message.get_all(header_name, []): 90 | if regexp.match(header_value): 91 | return True 92 | return False 93 | 94 | # Occasionally it's useful to tag messages based on a header value which notmuch 95 | # doesn't index. For the messages matching the query, assign the tag if the 96 | # message header exists and matches the given regexp. *Warning slow!* 97 | def tag_other_header_match(query_string, header_name, regexp, tags): 98 | regexp = re.compile(regexp) 99 | with notmuch.Database(mode=notmuch.Database.MODE.READ_WRITE) as db: 100 | query = db.create_query(query_string) 101 | for message in query.search_messages(): 102 | if header_matches(message.get_filename(), header_name, regexp): 103 | for tag in tags.split(): 104 | if tag[0] == "+": 105 | message.add_tag(tag[1:]) 106 | elif tag[0] == "-": 107 | message.remove_tag(tag[1:]) 108 | 109 | def update_database(): 110 | call(["notmuch", "new"]) 111 | 112 | def run_emacs_hook(hook_name): 113 | with open(os.devnull, "w") as devnull: 114 | call(["emacsclient", "-e", "(run-hooks '" + hook_name + ")"], 115 | stdout=devnull, stderr=devnull) 116 | -------------------------------------------------------------------------------- /postsync.py: -------------------------------------------------------------------------------- 1 | import davemail 2 | 3 | if __name__ == "__main__": 4 | # Take care to move messages which were tagged after synchronisation started. 5 | davemail.move_tagged_messages() 6 | # Now we can update the database. 7 | davemail.update_database() 8 | # Tidy up our tags, removing "new" and adding others based on folder. 9 | davemail.tag_moved_and_new_messages() 10 | davemail.tag_messages("tag:new AND from:feedback@slack.com AND " + 11 | "subject:'reminder'", "+muted") 12 | davemail.tag_other_header_match("tag:new", 13 | "X-Original-Delivered-to", 14 | ".*suboptimal\.co\.uk$", "+suboptimal") 15 | davemail.tag_muted_threads() 16 | davemail.tag_messages("tag:new", "-new") 17 | davemail.run_emacs_hook("notmuch-postsync-hook") 18 | -------------------------------------------------------------------------------- /presync.py: -------------------------------------------------------------------------------- 1 | import davemail 2 | 3 | if __name__ == "__main__": 4 | davemail.run_emacs_hook("notmuch-presync-hook") 5 | davemail.move_tagged_messages() 6 | -------------------------------------------------------------------------------- /syncmail: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while [ TRUE ];do 4 | if [ `nmcli networking connectivity` = 'full' ] 5 | then 6 | python3 presync.py && \ 7 | mbsync -a && \ 8 | python3 postsync.py 9 | fi 10 | echo 'Waiting...' 11 | read -t 120 12 | done 13 | --------------------------------------------------------------------------------