diff --git a/.gitignore b/.gitignore index b3c0db9..85e6e30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ *.swp -.DS_Store f/ +bin/mail-sync.sh **/newsboat/.newsboat/cache.db* **/newsboat/.newsboat/history.cmdline **/newsboat/.newsboat/ttrss-pw.txt diff --git a/mail/aerc/accounts.conf b/mail/aerc/accounts.conf new file mode 100644 index 0000000..356784c --- /dev/null +++ b/mail/aerc/accounts.conf @@ -0,0 +1,16 @@ +[migadu] +# +# Switching back to notmuch (from maildir experiment) +# +source = notmuch:///home/adam/Maildir +maildir-store = /home/adam/Maildir +outgoing = /home/adam/.local/bin/aerc-notmuch-send migadu +default = INBOX +from = Adam Cooper + +# notmuch does not support the copy-to directive +# copy-to = Sent +check-mail = 4m +check-mail-cmd = /home/adam/.local/bin/mail-sync.sh +check-mail-timeout = 20s + diff --git a/bin/aerc-notmuch-send b/mail/aerc/aerc-notmuch-send similarity index 100% rename from bin/aerc-notmuch-send rename to mail/aerc/aerc-notmuch-send diff --git a/mail/aerc/aerc.conf b/mail/aerc/aerc.conf new file mode 100644 index 0000000..a38bf1a --- /dev/null +++ b/mail/aerc/aerc.conf @@ -0,0 +1,191 @@ +# +# aerc main configuration + +[ui] +# +# Describes the format for each row in a mailbox view. This field is compatible +# with mutt's printf-like syntax. +# +# Default: %D %-17.17n %Z %s +index-format=%D %-17.17n %Z %s + +# +# See time.Time#Format at https://godoc.org/time#Time.Format +# +# Default: 2006-01-02 03:04 PM (ISO 8601 + 12 hour time) +timestamp-format=2006-01-02 15:04 + +# +# Width of the sidebar, including the border. +# +# Default: 20 +sidebar-width=20 + +# +# Message to display when viewing an empty folder. +# +# Default: (no messages) +empty-message=(no messages) + +# Message to display when no folders exists or are all filtered +# +# Default: (no folders) +empty-dirlist=(no folders) + +# Enable mouse events in the ui, e.g. clicking and scrolling with the mousewheel +# +# Default: false +mouse-enabled=false + +# +# Ring the bell when new messages are received +# +# Default: true +new-message-bell=true + +# Marker to show before a pinned tab's name. +# +# Default: ` +pinned-tab-marker='`' + +# Describes the format string to use for the directory list +# +# Default: %n %>r +dirlist-format=%n %>r + +# List of space-separated criteria to sort the messages by, see *sort* +# command in *aerc*(1) for reference. Prefixing a criterion with "-r " +# reverses that criterion. +# +# Example: "from -r date" +# +# Default: "" +sort=-r date + +# Moves to next message when the current message is deleted +# +# Default: true +next-message-on-delete=true + +fuzzy-complete=true + +[viewer] +# +# Specifies the pager to use when displaying emails. Note that some filters +# may add ANSI codes to add color to rendered emails, so you may want to use a +# pager which supports ANSI codes. +# +# Default: less -R +pager=less -R + +# +# If an email offers several versions (multipart), you can configure which +# mimetype to prefer. For example, this can be used to prefer plaintext over +# html emails. +# +# Default: text/plain,text/html +alternatives=text/html,text/plain + +# +# Default setting to determine whether to show full headers or only parsed +# ones in message viewer. +# +# Default: false +show-headers=false + +# +# Layout of headers when viewing a message. To display multiple headers in the +# same row, separate them with a pipe, e.g. "From|To". Rows will be hidden if +# none of their specified headers are present in the message. +# +# Default: From|To,Cc|Bcc,Date,Subject +header-layout=From|To,Cc|Bcc,Date,Subject|Labels + +# Whether to always show the mimetype of an email, even when it is just a single part +# +# Default: false +always-show-mime=false + +# How long to wait after the last input before auto-completion is triggered. +# +# Default: 250ms +completion-delay=250ms + +# +# Global switch for completion popovers +# +# Default: true +completion-popovers=true + +[compose] +# +# Specifies the command to run the editor with. It will be shown in an embedded +# terminal, though it may also launch a graphical window if the environment +# supports it. Defaults to $EDITOR, or vi. +editor= + +# +# Default header fields to display when composing a message. To display +# multiple headers in the same row, separate them with a pipe, e.g. "To|From". +# +# Default: To|From,Subject +header-layout=To|From,Subject + +# +# Specifies the command to be used to tab-complete email addresses. Any +# occurrence of "%s" in the address-book-cmd will be replaced with what the +# user has typed so far. +# +# The command must output the completions to standard output, one completion +# per line. Each line must be tab-delimited, with an email address occurring as +# the first field. Only the email address field is required. The second field, +# if present, will be treated as the contact name. Additional fields are +# ignored. +address-book-cmd=khard email --parsable --remove-first-line %s + +[filters] +# +# Filters allow you to pipe an email body through a shell command to render +# certain emails differently, e.g. highlighting them with ANSI escape codes. +# +# The first filter which matches the email's mimetype will be used, so order +# them from most to least specific. +# +# You can also match on non-mimetypes, by prefixing with the header to match +# against (non-case-sensitive) and a comma, e.g. subject,text will match a +# subject which contains "text". Use header,~regex to match against a regex. +subject,~^\[PATCH=awk -f /usr/local/share/aerc/filters/hldiff +text/html=/usr/local/share/aerc/filters/html +text/*=awk -f /usr/local/share/aerc/filters/plaintext +image/*=catimg -w $(tput cols) - + +[triggers] +# +# Triggers specify commands to execute when certain events occur. +# +# Example: +# new-email=exec notify-send "New email from %n" "%s" + +# +# Executed when a new email arrives in the selected folder +new-email=exec notify-send "New email from %n" "%s" + +[templates] +# Templates are used to populate email bodies automatically. +# + +# The directories where the templates are stored. It takes a colon-separated +# list of directories. +# +# default: /usr/local/share/aerc/templates/ +template-dirs=/usr/local/share/aerc/templates/ + +# The template to be used for quoted replies. +# +# default: quoted_reply +quoted-reply=quoted_reply + +# The template to be used for forward as body. +# +# default: forward_as_body +forwards=forward_as_body diff --git a/mail/aerc/binds.conf b/mail/aerc/binds.conf new file mode 100644 index 0000000..161904b --- /dev/null +++ b/mail/aerc/binds.conf @@ -0,0 +1,106 @@ +# Binds are of the form = +# To use '=' in a key sequence, substitute it with "Eq": "" +# If you wish to bind #, you can wrap the key sequence in quotes: "#" = quit + = :prev-tab + = :next-tab + = :term + +[messages] +q = :quit + +j = :next + = :next + = :next 50% + = :next 100% + = :next -s 100% + +k = :prev + = :prev + = :prev 50% + = :prev 100% + = :prev -s 100% +g = :select 0 +G = :select -1 + +J = :next-folder +K = :prev-folder + +v = :mark -t +V = :mark -v + + = :view +# Replace deletion key bindings to move items to the trash +# d = :prompt 'Really delete this message?' 'delete-message' +# D = :delete +d = :mv Trash +D = :mv Trash +A = :archive flat + +C = :compose + +rr = :reply -a +rq = :reply -aq +Rr = :reply +Rq = :reply -q + +c = :cf +$ = :term +! = :term +| = :pipe + +/ = :search +\ = :filter +n = :next-result +N = :prev-result + +[view] +q = :close +| = :pipe +D = :delete +S = :save +A = :archive flat + +f = :forward +rr = :reply -a +rq = :reply -aq +Rr = :reply +Rq = :reply -q + +H = :toggle-headers + = :prev-part + = :next-part +J = :next +K = :prev + +[compose] +# Keybindings used when the embedded terminal is not selected in the compose +# view +$ex = + = :prev-field + = :next-field + = :next-field + +[compose::editor] +# Keybindings used when the embedded terminal is selected in the compose view +$noinherit = true +$ex = + = :prev-field + = :next-field + = :prev-tab + = :next-tab + +[compose::review] +# Keybindings used when reviewing a message to be sent +y = :send +n = :abort +p = :postpone +q = :abort +e = :edit +a = :attach + +[terminal] +$noinherit = true +$ex = + + = :prev-tab + = :next-tab diff --git a/mail/aerc/mail-sync.sh.example b/mail/aerc/mail-sync.sh.example new file mode 100755 index 0000000..0cf5e82 --- /dev/null +++ b/mail/aerc/mail-sync.sh.example @@ -0,0 +1,37 @@ +#!/bin/sh + +OFFLINEIMAP=$(pgrep offlineimap) +NOTMUCH=$(pgrep notmuch) + +if [ -n "$OFFLINEIMAP" ] || [ -n "$NOTMUCH" ]; then + echo "Already running one instance of offlineimap or notmuch. Exiting..." + exit 0 +fi + +echo "Deleting messages tagged as *deleted*" +notmuch search --format=text0 --output=files tag:deleted | xargs -0 --no-run-if-empty rm -v + +offlineimap -o +notmuch new + +# retag all "new" messages "inbox" and "unread" +notmuch tag +inbox +unread -new -- tag:new + +# mailing lists +notmuch tag +mailinglist -- to:xmonad@haskell.org +notmuch tag +mailinglist -- to:guile-user@gnu.org +notmuch tag +mailinglist -- to:zsh-users@zsh.org +notmuch tag +mailinglist -- to:qutebrowser@lists.qutebrowser.org +notmuch tag +mailinglist -- to:~rjarry/aerc-discuss@lists.sr.ht + +# TODO: Confirm that we need to do this +# move tagged items across folders and retag +notmuch search --output=files tag:trash and not folder:Trash | xargs mv -t /home/adam/Maildir/Trash/cur/ +notmuch tag +trash -inbox -sent -archive -junk -drafts -- folder:Trash and not tag:trash + +# move unimportant institutional messages to the trash after a couple of days +notmuch tag +trash -inbox -- date:..2d and tag:mailinglist +notmuch tag +trash -inbox -- date:..2d and from:@facebookmail.com +notmuch tag +trash -inbox -- date:..2d and from:noreply@twitch.tv +notmuch tag +trash -inbox -- date:..2d and from:messages-noreply@linkedin.com + diff --git a/mail/notmuch/.notmuch-config b/mail/notmuch/.notmuch-config new file mode 100644 index 0000000..94b5eb0 --- /dev/null +++ b/mail/notmuch/.notmuch-config @@ -0,0 +1,87 @@ +# .notmuch-config - Configuration file for the notmuch mail system +# +# For more information about notmuch, see https://notmuchmail.org + +# Database configuration +# +# The only value supported here is 'path' which should be the top-level +# directory where your mail currently exists and to where mail will be +# delivered in the future. Files should be individual email messages. +# Notmuch will store its database within a sub-directory of the path +# configured here named ".notmuch". +# +[database] +path=/home/adam/Maildir + +# User configuration +# +# Here is where you can let notmuch know how you would like to be +# addressed. Valid settings are +# +# name Your full name. +# primary_email Your primary email address. +# other_email A list (separated by ';') of other email addresses +# at which you receive email. +# +# Notmuch will use the various email addresses configured here when +# formatting replies. It will avoid including your own addresses in the +# recipient list of replies, and will set the From address based on the +# address to which the original email was addressed. +# +[user] +name=Adam Cooper +primary_email=adam@theadamcooper.com + +# Configuration for "notmuch new" +# +# The following options are supported here: +# +# tags A list (separated by ';') of the tags that will be +# added to all messages incorporated by "notmuch new". +# +# ignore A list (separated by ';') of file and directory names +# that will not be searched for messages by "notmuch new". +# +# NOTE: *Every* file/directory that goes by one of those +# names will be ignored, independent of its depth/location +# in the mail store. +# +[new] + +# Search configuration +# +# The following option is supported here: +# +# exclude_tags +# A ;-separated list of tags that will be excluded from +# search results by default. Using an excluded tag in a +# query will override that exclusion. +# +[search] + +# Maildir compatibility configuration +# +# The following option is supported here: +# +# synchronize_flags Valid values are true and false. +# +# If true, then the following maildir flags (in message filenames) +# will be synchronized with the corresponding notmuch tags: +# +# Flag Tag +# ---- ------- +# D draft +# F flagged +# P passed +# R replied +# S unread (added when 'S' flag is not present) +# +# The "notmuch new" command will notice flag changes in filenames +# and update tags, while the "notmuch tag" and "notmuch restore" +# commands will notice tag changes and update flags in filenames +# +[maildir] +synchronize_flags=true + +[index] +header.Context-Transfer-Encoding=Context-Transfer-Encoding diff --git a/mail/offlineimap/config b/mail/offlineimap/config new file mode 100644 index 0000000..1840345 --- /dev/null +++ b/mail/offlineimap/config @@ -0,0 +1,25 @@ +[general] +accounts = main +pythonfile = /home/adam/.config/offlineimap/offlineimap.py +maxsyncaccounts = 1 + +[Account main] +localrepository = main-local +remoterepository = main-remote +# autorefresh = 0.5 +# quick = 10 + +[Repository main-local] +type = Maildir +localfolders = ~/Maildir + +[Repository main-remote] +type = IMAP +remotehost = imap.migadu.com +remoteuser = adam@theadamcooper.com +remotepasseval = get_pw_from_attrs("service", "migadu") +starttls = yes +ssl = yes +sslcacertfile = /etc/ssl/certs/ca-certificates.crt +# keepalive = 60 +# holdconnectionopen = yes diff --git a/mail/offlineimap/offlineimap.py b/mail/offlineimap/offlineimap.py new file mode 100644 index 0000000..5034697 --- /dev/null +++ b/mail/offlineimap/offlineimap.py @@ -0,0 +1,151 @@ +''' +offlineimap.py +This provides a handful of functions for retrieving secrets from GNOME Keyring +using the libsecret API. See the documentation for each function +''' + +from gi import require_version +require_version('Secret', '1') +from gi.repository import Secret + +def get_pw_from_desc(pw_desc) : + ''' + This function returns the password for an item in the default keyring + which contains the description provided. + Use this function if you created a password using the dialogue in Seahorse + ''' + # Get service + service = Secret.Service.get_sync(Secret.ServiceFlags.LOAD_COLLECTIONS) + + # Get default keyring + keyring = Secret.Collection.for_alias_sync(service, "default", \ + Secret.CollectionFlags.NONE, None) + + # Get keyring items + items = keyring.get_items() + + # Load secrets + Secret.Item.load_secrets_sync(items) + + # Loop through items, find the matching one and return its password + password = None + for item in items : + if item.get_label() == pw_desc : + password = item.get_secret().get_text() + break + + # Close connection + service.disconnect() + + return password + +def get_pw_from_attrs(*attr_val_pairs) : + ''' + This function returns the password for an item in the default keyring + which contains all of the attribute value pairs provided as arguments. + Use this function if you created a password using the secret-tool command + or another such program that interfaces with libsecret + ''' + # Check the list of attr-val pairs is present and contains an even number + # of elements + if attr_val_pairs == () : + raise TypeError("get_pw_from_attrs() at least 1 attribute-value pair " \ + "must be supplied") + if len(attr_val_pairs) % 2 != 0 : + raise TypeError("get_pw_from_attrs() incomplete attribute-value " \ + "pair was supplied") + + # Get service + service = Secret.Service.get_sync(Secret.ServiceFlags.LOAD_COLLECTIONS) + + # Get default keyring + keyring = Secret.Collection.for_alias_sync(service, "default", \ + Secret.CollectionFlags.NONE, None) + + # Get keyring items + items = keyring.get_items() + + # Load secrets + Secret.Item.load_secrets_sync(items) + + # Loop through items, find the one which contains all supplied attr_val + # pairs and return its password + password = None + for item in items : + attrs = item.get_attributes() + match = True + for x in range(0, len(attr_val_pairs), 2) : + key = attr_val_pairs[x] + value = attr_val_pairs[x + 1] + try : + if attrs[key] != value : + match = False + break + except KeyError : + match = False + break + if match : + password = item.get_secret().get_text() + break + + # Close connection + service.disconnect() + + return password + +def get_val_from_attrs(attr, *attr_val_pairs) : + ''' + This function returns the value for a given attribute. The first item + found that contains that attribute will be the one that is used. To ensure + that the correct item is chosen, any number of attribute-value pairs can + be optionally supplied as arguments and only the item which contains all + of those attr-val pairs (along with the main attr) will be used. + Use this function if you created a password using the secret-tool command + or another such program that interfaces with libsecret + ''' + # Check the list of attr-val pairs contains an even number of elements + # if it exists + if attr_val_pairs != () : + if len(attr_val_pairs) % 2 != 0 : + raise TypeError("get_val_from_attrs() incomplete attribute-value " \ + "pair was supplied") + + # Get service + service = Secret.Service.get_sync(Secret.ServiceFlags.LOAD_COLLECTIONS) + + # Get default keyring + keyring = Secret.Collection.for_alias_sync(service, "default", \ + Secret.CollectionFlags.NONE, None) + + # Get keyring items + items = keyring.get_items() + + # Loop through items, find the one which contains the supplied attribute + # (plus any attr_val pairs if specified) and return that attribute's + # value + attr_value = None + for item in items : + attrs = item.get_attributes() + try : + attrs[attr] + except KeyError : + continue + match = True + for x in range(0, len(attr_val_pairs), 2) : + key = attr_val_pairs[x] + value = attr_val_pairs[x + 1] + try : + if attrs[key] != value : + match = False + break + except KeyError : + match = False + break + if match : + attr_value = attrs[attr] + break + + # Close connection + service.disconnect() + + return attr_value