jaromail

a commandline tool to easily and privately handle your e-mail
git clone git://parazyd.org/jaromail.git
Log | Files | Refs | Submodules | README

commit b5f7004e0b82808f07c8fa504cbcf8840ed2160e
parent 19de22114e68d351ae6e60d18a79eb71a53508d9
Author: Jaromil <jaromil@dyne.org>
Date:   Tue,  3 Nov 2015 01:23:48 +0100

Command redesign to leverage pipe operations

This is a rather big refactoring of the way commanands work and
interoperate, so that one can pipe inputs and outputs of commands
efficiently to achieve more complex operations, i.e: take the results of
a search command, extract all email addresses found in them and export
all as a vcard.

Documentation in the user manual is updated accordingly. This
refactoring also eliminates quite some duplicate and obsolete code.

Diffstat:
MTODO.md | 2+-
Mdoc/jaromail-manual.org | 225++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/jaro | 121++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/zlibs/addressbook | 151+++++++++++++++++++++++++------------------------------------------------------
Msrc/zlibs/email | 4+---
Msrc/zlibs/helpers | 63+++++++--------------------------------------------------------
Msrc/zlibs/parse | 241++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/zlibs/search | 6++++++
8 files changed, 386 insertions(+), 427 deletions(-)

diff --git a/TODO.md b/TODO.md @@ -22,7 +22,7 @@ Serve maildirs to all kinds of imap clients (MUA) locally ## DIME specification Jaro Mail will support DIME -https://darkmail.info/downloads/dark-internet-mail-environment-december-2014.pdf +https://darkmail.info/downloads/dark-internet-mail-environment-december-201.4pdf ## Vacation trigger sieve script example diff --git a/doc/jaromail-manual.org b/doc/jaromail-manual.org @@ -1,6 +1,6 @@ -#+TITLE: Jaro Mail 3.2 +#+TITLE: Jaro Mail 3.3 #+AUTHOR: by Jaromil @ dyne.org -#+DATE: Jan 2015 +#+DATE: November 2015 #+OPTIONS: H:3 num:t toc:t \n:nil @:t ::t |:t ^:nil f:t TeX:t #+EXCLUDE_TAGS: noexport @@ -354,67 +354,61 @@ If you have configured the *keep* option, which is the default, Jaro Mail will o : jaro -This will open the first folder containing unread emails, starting from -the *known* folder, then *priv*, then all the destinations specified by *Filters.txt* exactly in the ascending order they are listed in that configuration file.. +This will launch mutt on the first folder containing unread emails, starting from the *known* folder, then *priv*, then all the destinations specified by *Filters.txt* exactly in the ascending order they are listed in that configuration file.. From there on, pressing *=* or *c* you can change to other folders and your *unsorted* and *unsorted.ml* mails. ** Write a new mail -If you like to write a mail to someone, just write his/her own address -as an argument to Jaro Mail +If you like to write a mail to someone, hit *m* and write the recipient address, you will be then asked about any additional Cc: recipients. -: jaro compose friend@home.net +If you don't remember the emails of the recipients, you can just type their name or parts of the email you remember, then press the [ *Tab* ] +key for completion. A list of addresses may popup with matches found in your whitelist addressbook to help remind who are you looking for. -But if you don't remember the email of your friend, then you can just -start *jaro compose* without options, then start typing the -name or whatever you remember of it: pressing the *Tab* key a -completion will help to remind what you are looking for, offering a -list of options to choose from, taken from your whitelist addressbook. +The email is composed using a special [[http://www.vim.org/][Vim]] configuration that facilitates justifying text to 72 columns using [ *ctrl-j* ]. After composing the email you will be able to review it and change: -If you are writing an email with attachments (and you are sure their -size is reasonably small to be circulated via email) you can launch -Jaro Mail with files as arguments, or even wildcards, and they will be -automatically set as attachments, you can then specify its recipients + - the From: field using [ *ESC f* ] + - the recipient in the To: field using [ *t* ] + - the recipients in the Cc: field using [ *c* ] + - the subject string using [ *s* ] -: jaro picture01.jpg jingle02.mp3 ~/myicons/* +You'll also be able to add more attachments by pressing *a* and use the arrow keys to move over the existing ones and delete them using [ *D* ] (please note that is a uppercase D, because lowercase d will just add a description for the attachment). -Will send a mail with various separate attachments (using MIME -encapsulation): a picture, an hopefully small audio file and a list of -icons which are all the files contained into the myicons/ directory. - -The email is composed using a special [[http://www.vim.org/][Vim]] configuration that facilitates justifying text to 72 columns using *ctrl-j*. After composing the email you will be able to review it, change the From: field (*ESC f*), the recipient on To: (*t*), add recipients in Cc: (*c*), change the subject string (*s*), add more attachments (*a*) or move over the existing ones and delete them (*D*). At last, when ready, pressing *y* will queue the email into the outbox, ready for sending. -One can review at any time the sending queue (*outbox*), which is just another maildir from which emails can also be deleted to abort sending them: +One can review at any time the sending queue, which is just another maildir named *outbox* : jaro outbox -Once sure the outbox contains emails that need to be sent, make sure the computer is connected to the Internet and issue the *send* command: +Mails can be deleted from this view using [ *d* ] or edited using [ *e* ] which will allow tweaking of both the header and body of the email. + +Once sure the outbox contains all what needs to be sent, make sure the computer is connected to the Internet and issue the *send* command: : jaro send -Jaro Mail will send all emails in outbox, one by one, listing their recipients and size while doing so. If successful, mails will be removed from the outbox and put into the *sent* folder. +Jaro Mail will send all emails in outbox, one by one, listing their recipients and size while doing so. If successful, mails will be removed from the outbox and put into the *sent* folder, which can be accessed from inside mutt or with the command *jaro open sent*. + +** Write a new email from the commandline + +Jaro Mail supports a lot of commandline operations based on stdin/stdout pipes, which makes it pretty easy to use in scripts that send emails and attachments. + +If you have written a plain-text email using your favorite editor, you can send it quickly using the commandline: save the email into a txt file and then pipe it into *jaro compose* followed by a list of recipients and, optionally a list of filenames to attach. For example: + +: cat Greetings.txt | jaro compose friends@dyne.org picture01.jpg jingle02.mp3 ~/myicons/* + +The command above may send an email with various separate attachments (using MIME encapsulation): a picture, an hopefully small audio file and a list of icons which are all the files contained into the myicons/ directory. In this case the recipient will be friends@dyne.org, but may be any other email address found on the commandline in any position. + +Once executed you will find this email in *jaro outbox*, ready to be reviewed and sent with *jaro send*. ** Reply messages -While browsing through the index of emails in various folders, one can -reply any of them just by pressing the [ *r* ] key, which will ask if -the original message should be quoted and then open your favorite -editor to compose your text. +While browsing through the index of emails in various folders, one can reply any of them just by pressing the [ *r* ] key, which will ask if +the original message should be quoted and then open your favorite editor to compose your text. -If the email you are replying has been sent to multiple recipients -(for instance using multiple addresses in the Cc: or From: fields) -they will all be included, but you will have the possibility to -exclude them by hand editing those fields before queuing to outbox, as explained in the previous paragraph. +If the email you are replying has been sent to multiple recipients (for instance using multiple addresses in the Cc: or From: fields) they will all be included, but you will have the possibility to exclude them by hand, editing the Cc: field. To remove them all at once use [ *ctrl-k* ] just like deliting a line on the terminal. -It is also possible to forward a message to someone else than the -sender, for instance to submit it to his or her attention, or that of -a mailinglist. To do that, you can use the [ *f* ] key which will -present you with the full message and the possibility to write -something on top of it, to describe its contents to its new -recipients. Forwards include all attachments and are sent as attachments themselves, but this behavious can be changed as a confirmation to "send forward as attach" is asked. +It is also possible to forward a message to someone else than the sender, for instance to submit it to his or her attention, or that of a mailinglist. To do that, you can use the [ *f* ] key which will present you with the full message and the possibility to write something on top of it, to describe its contents to its new recipients. Forwards include all attachments and are sent as attachments themselves, but this behavious can be changed as a confirmation to "send forward as attach" is asked. ** Peek without downloading anything @@ -423,40 +417,27 @@ them, then you can use the *peek* function: : jaro peek -This will open the default configured IMAP account and folder over SSL -protocol (securing the data transfer) and show your emails. +This will open the default configured IMAP account and folder over SSL protocol (securing the data transfer) and allow you to browse, read and reply your emails without downloading them. -From peek you can reply and even delete emails, but be careful since -what you delete here will be removed from the server and won't be +Using peek you can reply and even delete emails, but be careful since what you delete here will be removed from the server and won't be there when you download it from home. -This functionality can be also very useful if you are from a slow -connection and need to delete some email that is clogging it and that -you are not able to download because of its size. +This functionality can be also very useful if you are from a slow connection and need to delete some email that is clogging it and that you are not able to download because of its size. -The peek command will automatically open the INBOX, but also other remote imap folders can be specified, like for instance *priv* or *unsorted*, in case the sieve filters generated by Jaro Mail are uploaded on the server. To have a list of imap folders on the server a command is also available: +The peek command will automatically open the INBOX, but also other remote imap folders can be specified, like for instance *priv* or *unsorted* if whitelisting is also setup server-side (the sieve filters generated by Jaro Mail need to be uploaded on the server). To have a list of imap folders on the server a command is also available: : jaro imap listfolders +Will list on the terminal all folders found on the imap account, one per line. ** Save important emails for later -Sometimes one can be on the rush while reading emails (local or via -imap) and flagging them as important can be useful to keep focus on -priorities. In some cases it is very useful to save such important -messages locally for later reference, for instance in a folder keeping -messages that need to be remembered and that will constitute a kind of -TODO list (a'la GTD). - -Jaro Mail implements such functionalities: by pressing the [ *F* ] key -(shift-f) one can flag an email, which will turn bright-green in the -index. In addition to that there is a folder called *remember/* where -one can copy emails on the fly using the [ *R* ] key (shift-r) any -time. Messages will be duplicated into the remember folder (which of -course can be opened with the command *jaro remember*) so they can -also be edited with annotations on the task they refer to, for -instance using the [ *e* ] key, without affecting the original -message. +Sometimes one can be on the rush while reading emails (local or via imap) and flagging them as important can be useful to keep focus on +priorities. In some cases it is very useful to save such important messages locally for later reference, for instance in a folder keeping messages that need to be remembered and that will constitute a kind of TODO list (a'la GTD). + +Jaro Mail implements such functionalities: by pressing the [ *F* ] key (uppercase) one can flag an email, which will turn bright-green in the +index. In addition to that there is a folder called *remember/* where one can copy emails on the fly using the [ *R* ] key (uppercase) any time. Messages will be duplicated into the remember folder (which of course can be opened with the command *jaro remember*) so they can +also be edited with annotations on the task they refer to, for instance using the [ *e* ] key, without affecting the original message. ** Workflow in brief @@ -467,6 +448,7 @@ Below a recapitulation of keys commonly used in our workflow | *m* | Compose a new message | | *Tab* | Complete addresses and folders input | | *r* | Reply to the sender of a message | +| *d* | Delete a message | | *y* | Send a message (queue in outbox) | | *f* | Forward a message to new recipients | | *=* | List all filtered maildir folders | @@ -483,102 +465,121 @@ Addressbooks are the files storing the whitelist, the blacklist and optionally o Addressbooks can be edited using a interactive console interface, for instance to add or delete entries by hand: use the *abook* command and optionally the *-l* option. -: jaro abook -l whitelist +: jaro abook -This will open the current whitelist for edit. To edit the blacklist use *-l blacklist* instead. +This will open the current whitelist for edit. To edit the blacklist add *-l blacklist* instead. -To quickly dump to the console all names and addresses in the Jaro -Mail addressbook, one can use the *extract* command +To quickly dump to the console all names and addresses in the Jaro Mail addressbook, one can use the *list* command -: jaro extract -l whitelist +: jaro list -To match a string across the addressbook, simply use the composite -command *search addr* followed by strings, for instance: +To match a string across the addressbook, simply use the composite command *addr* followed by strings, for instance: -: jaro search addr dyne -l whitelist +: jaro addr dyne will list all addresses containing 'dyne' in your whitelist. ** Address lists -Jaro Mail makes it easy to handle lists of addresses as plain text *address lists* composed by a '/Name <email>/' entries on each new line. - -Entries inside address lists are newline separated strings conforming to the RFC822 standard and their charset encoding must be UTF-8. We use this simple interchange format of address lists as input or output of various commands, taking advantage of console piping from stdin to stdout. +Jaro Mail handles lists of addresses as plain text files or streams with entries formatted as '/Name <email>/' and newline terminated. This simple format conforms (or is normalized to) the RFC822 standard and UTF-8 charset encoding, both produced on /stdout/ and read from /stdin/ by various useful commands to take advantage of console piping. -Address lists are the output of the previously mentioned *search addr* command, as well of the *extract* command: +Such lists of addresses are the output of the *extract* command, which is able to read the output of other commands and extract a list of email addresses found. -: jaro extract -l whitelist +: jaro search open source date:2w.. | jaro extract -Will print to stdout the address list of all entries in the whitelist addressbook, one on each new line. +Will print to stdout the list of addresses found among the results of a search for /open source/ through all the emails archived in the past 2 weeks -: jaro extract date:1y.. and folder:known +: jaro search date:1y.. and folder:known | jaro extract -Will print the address list of all unique addresses in the headers of emails found by the search expression '/date:1y.. and folder:known/', matching all messages stored in the '/known/' folder and not older than 1 year. +Will print a sorted list of unique addresses found in the emails matching the search expression '/date:1y.. and folder:known/', meaning all messages stored in the '/known/' folder and not older than 1 year from now. -: jaro extract priv +The *import* command is complementary to extraction: it reads an address list from stdin and imports it inside an addressbook specified using '-l' or a /group/ list file provided as argument. -Will print the address list of all unique addresses contained in the headers of emails stored in the maildir '/priv/', which is found in $JAROMAILDIR. A full path to a maildir outside of $JAROMAILDIR can also be used. - -The *import* command is complementary to extraction: it reads an address list from stdin and imports it inside an addressbook specified using '-l' or an address list file provided as argument, removing duplicates. - -: jaro extract unsorted | jaro import -l blacklist +: jaro search folder:unsorted | jaro extract | jaro import -l blacklist Will extract all addresses found in unsorted (the maildir collecting all non-mailinglist emails in which we are not an explicit recipient) and put them into our blacklist. -** VCards +** Export to VCard and other formats + +VCard is an exchange format useful to interface with other addressbook software and mobile phones, as well with spyware as Google and Apple mail. Jaro Mail supports converting address lists to a variety of formats thanks to /abook/: -VCard is an exchange format useful to interface with other addressbook software and mobile phones. Jaro Mail supports is via the *extract* command followed by a vcard file argument: +: jaro addr | jaro export vcard -: jaro extract 0001.vcard +Will take the list of addresses in whitelist and convert it to the *vcard* format on stdout, ready to be redirected to a file. -Will print out the address list of all entries found in the file '/0001.vcard/'. +Here below a list of output formats supported as argument to export: -The special command *vcard* can be used to convert an address list from stdin to a VCard file, exporting entries used inside Jaro Mail to a format supported by the majority of addressbook programs: +| Format | Description | +|---------+-------------------------------------| +| abook | abook native format | +| ldif | ldif / Netscape addressbook (.4ld) | +| vcard | vCard 2 file | +| mutt | mutt alias | +| muttq | mutt query format (internal use) | +| html | html document | +| pine | pine addressbook | +| csv | comma separated values | +| allcsv | comma separated values (all fields) | +| palmcsv | Palm comma separated values | +| elm | elm alias | +| text | plain text | +| wl | Wanderlust address book | +| spruce | Spruce address book | +| bsdcal | BSD calendar | +| custom | Custom format | -: jaro extract -l whitelist | jaro vcard > whitelist.vcard +Of course *export* works with any list of addresses from stdin, for instance the result of *extract* operations on search queries, so that multiple commands can be concatenated. -Will save in the file '/whitelist.vcard'/ all addresses stored inside the whitelist addressbook. This is done concatenating multiple commands: the address list extracted from the whitelist is piped as stdin to the vcard command, whose output is redirected to a file. ** Addressbook in brief Here a roundup on the addressbook commands that are available from the /jaro/ commandline script. Arguments '-l abook' take the string to identify -| Command | Arguments | Function (print on stdout, import from stdin) | -|---------------+-------------+--------------------------------------------------| -| *abook* | -l listname | edit the addressbook (default whitelist) | -| *extract* | -l listname | print address list of all entries in addressbook | -| *extract* | search expr | print address list of messages found by search | -| *extract* | maildir | print address list of all mails in maildir | -| *extract* | gpg keyring | print address list of gpg public keyring | -| *extract* | gpg pubkey | print address list of gpg key signatures | -| *extract* | vcard file | print address list of entries in VCard file | -| *vcard* | vcard file | export the addressbook into a VCard file | -| *import* | -l listname | import address list from stdin to addressbook | -| *import* | filename | import address list into an address list file | -| *search addr* | (-l) string | print address list of matches in addressbook | +| Command | Arguments | Function (print on stdout, import from stdin) | +|-----------+-------------+--------------------------------------------------| +| *abook* | -l listname | edit the addressbook (default whitelist) | +| *addr* | search expr | print list of addresses matching expression | +| *extract* | maildir | print address list of all mails in maildir | +| *extract* | gpg keyring | print address list of gpg public keyring | +| *extract* | gpg pubkey | print address list of gpg key signatures | +| *extract* | vcard file | print address list of entries in VCard file | +| *import* | -l listname | import address list from stdin to addressbook | +| *export* | format | convert address list to a format (default vcard) | * Searching -Searching across all your emails it is as important as demanding of a task. Jaro Mail implements it using Notmuch which is relying on the Xapian search engine. To index and tag all your downloaded emails use: +Searching across all your emails it is as important as demanding of a task. Jaro Mail implements it using [[https://notmuchmail.org/][Notmuch]] which is relying on the [[http://xapian.org][Xapian]] search engine, completely relying on local computations made on your machine, there is no data at all being communicated on-line. + +To index and tag all your emails that are locally archived in Jaro Mail use: : jaro index -This will take a while and increase the size of the storage, but will definitely come useful when in need of searching rapidly across all available emails. +This will take a while and increase the size of the storage of about one sixth of its total occupation, but will definitely come useful when in need of searching rapidly across all available emails. To run a search for emails containing the '/open source/' string, do + +: jaro search open source + +To search for all emails containing this string and dated between now and the last two weeks, do + +: jaro search open source date:2w.. + +The search command prints out a list of found filenames which may be useful to a script, but less useful to a human. In order to read a quick summary of the emails found it is possible to pipe the results into the *headers* command which will print out date, sender and subject of each file + +: jaro search open source date:2w.. | jaro headers Searching has also an interactive interface called *alot* which pops up to show search results and browse through them, refine the terms and in general operate on emails with the usual keys. One can also reply to emails directly from alot: -: jaro search +: jaro alot search expression strings folder:known -To restrict the search to a single folder, one can use the *folder:* prefix to search terms. Tags can be used also with *tag:* as well dates can be specified with ranges using *date:*. Consecutive string expressions are aloud to refine the search match, connected with logical and/or, plus also the header to search can be indicated, as for instance *from:* or *to:*. Read more about this below in the /Search term/ and /Date and time search/ sections (extracts from the *notmuch-search-terms* manpage). +To restrict the search to a single folder, one can use the *folder:* prefix to search terms. Tags can be used also with *tag:* as well dates can be specified with ranges using *date:*. Consecutive string expressions are aloud to refine the search match, connected with logical and/or, plus also the header to search can be indicated, as for instance *from:* or *to:*. Read more about this below in the /Search term/ and /Date and time search/ sections (extracts from the *notmuch-search-terms* manpage) and on the notmuch webpage at http://notmuchmail.org +With the *addr* command the search will be run on the whitelist addressbook entries instead of actual email contents. -If the first argument following the search command is *addr* then the search will be run on the whitelist addressbook entries instead. Also the blacklist can be searched this way using *-l blacklist*: +: jaro addr joe -: jaro search addr -l blacklist spammer-joe +Will list all addresses matching the string 'joe' inside the /whitelist/ addressbook. Also the blacklist can be searched this way adding the switch *-l blacklist*: -Will list all addresses matching the string 'spammer-joe' inside the /blacklist/ addressbook. ** Combining terms diff --git a/src/jaro b/src/jaro @@ -122,7 +122,7 @@ PARAM=() typeset -A global_opts typeset -A opts -typeset -h global_quit +vars+=(global_quit) global_quit=0 # global variable for account selection @@ -150,8 +150,14 @@ vars+=(e_parsed) arrs+=(maildirs) vars+=(last_deliver) -# global arrays for search -arrs+=(search_results) +# stdin when read becomes globally accessible +vars+=(stdin bytesread) +stdin="" +bytesread=0 + +# global arrays for search results and mailpaths +# are all arrays of absolute paths +arrs+=(search_results mailpaths) search_results=() # global variable for mutt binary @@ -278,6 +284,14 @@ hostname=$(hostname) # gather the current hostname } # if not sourcing _mutt() { + + [[ -r $MAILDIRS/.mutt/rc ]] || { + error "Jaro Mail is not yet configured." + error "To configure, edit the files in $MAILDIRS/Accounts" + error "Then run 'jaro update' at least once." + return 1 + } + for i; do _fa+=" $i "; done func "exec: mutt -F $MUTTDIR/rc ${=muttflags} ${_fa}" [[ "$subcommand" = "peek" ]] || rm -f $MUTTDIR/muttpass @@ -490,6 +504,8 @@ main() { subcommands_opts[list]="" subcommands_opts[extract]="" subcommands_opts[parse]="" + subcommands_opts[header]="" + subcommands_opts[headers]="" subcommands_opts[import]="" subcommands_opts[export]="" @@ -546,17 +562,9 @@ main() { subcommand="__default" fi - if [[ -z ${(k)subcommands_opts[$subcommand]} ]]; then - # unknown command, pass it to autostart - func "unknown command, autostart: $@" - autostart ${=@} - exitcode=$? - [[ $exitcode = 0 ]] || { - error "command \"$subcommand\" not recognized" - act "try -h for help" - } - return $exitcode - fi + # if [[ -z ${(k)subcommands_opts[$subcommand]} ]]; then + # # unknown command, pass it to autostart + # fi argv=(${oldstar}) unset oldstar @@ -696,10 +704,7 @@ main() { exitcode=$? ;; -# stat) stats ${PARAM} ;; - - - addr) search_addressbook ${PARAM} ;; + addr|list) search_addressbook ${PARAM} ;; complete) complete ${PARAM} exitcode=$? @@ -717,20 +722,8 @@ main() { exitcode=$? ;; - vcard) - export_vcard ${PARAM} - ;; - "export") - case "$PARAM" in - abook) - notice "Exporting old addressbook to new format" - list=whitelist; export_abook - list=blacklist; export_abook - ;; - vcard) export_vcard ;; - *) export_vcard ;; - esac + export_vcard ${PARAM} ;; abook) edit_abook ${PARAM} @@ -778,10 +771,6 @@ main() { exitcode=$? ;; 'source') return 0 ;; - __default) func "no command provided" - autostart ${PARAM} - exitcode=$? - ;; imap) imapcmd="$1" @@ -816,18 +805,18 @@ main() { esac ;; - parse) - extract_stdin ${PARAM} - exitcode=$? - ;; + # list) + # list_abook ${PARAM} + # exitcode=$? + # ;; - list) - list_abook ${PARAM} + extract|parse) + extract_addresses ${PARAM} | sort | uniq exitcode=$? ;; - extract) - extract_auto ${PARAM} + header|headers) + extract_headers ${PARAM} exitcode=$? ;; @@ -855,14 +844,46 @@ main() { exitcode=$? ;; + # __default) func "no command provided" + # autostart ${PARAM} + # exitcode=$? + # ;; *) # unknown command, pass it to autostart - func "unknown command, remote check" - autostart ${PARAM} - exitcode=$? - [[ $exitcode = 0 ]] || { - error "command \"$subcommand\" not recognized" - act "try -h for help" - } + func "no command, autostart" + + # argument passed: determine if an email + if isemail "$subcommand"; then + notice "Composing message to: ${@}" + # its an email, TODO see if we have it in our addressbook + _mutt ${=@} + exitcode=0 + # or a directory of file + elif [[ -r "$subcommand" ]]; then + # is it a maildir? then open + maildircheck ${subcommand} + [[ $? = 0 ]] && { + _mutt -f ${subcommand} + exitcode=0 + } + # is it a regular file? then attach it + [[ -f "$subcommand" ]] && { + _mutt -a ${=@} + exitcode=0 + } + # or the name of a folder in Jaro Mail + elif [[ -r "$MAILDIRS/$subcommand" ]]; then + maildircheck "$MAILDIRS/$subcommand" && { + notice "Opening folder ${subcommand=}" + _mutt -f "$MAILDIRS/${subcommand}" + exitcode=0 + } + else + + # just open mutt on first unread folder + _mutt -Z + exitcode=$? + + fi ;; esac diff --git a/src/zlibs/addressbook b/src/zlibs/addressbook @@ -296,115 +296,43 @@ import() { _arg=${PARAM[1]} func "import() arg: $_arg (param: $PARAM)" - # a map to eliminate duplicates - typeset -AU result - - [[ "$_arg" = "" ]] && { - notice "Import address list from stdin into addressbook $list" - _stdin=`cat` # reads into var _stdin - _new=0 - act "imported new entries will be printed on stdout" - e_addr=() - for i in ${(f)_stdin}; do - [[ $global_quit = 1 ]] && break - - # skip comments starting with # - [[ "$i[1]" = "#" ]] && continue - - print - "From: $i" | e_parse From - [[ $? = 0 ]] || continue - - _e="${e_parsed[(ws:,:)1]:l}" - - # check if the email is not already known - lookup_email "$_e" - [[ $? = 0 ]] && { - func "email already known: $_e" - continue - } - _n="${e_parsed[(ws:,:)2]}" + notice "Import address list from stdin into addressbook $list" - print - "$_n <$_e>" - [[ $DRYRUN = 0 ]] && insert_address "$_e" "$_n" + _new=0 + act "imported new entries will be printed on stdout" - _new=$(( $_new + 1 )) - done - notice "Valid unique entries parsed: ${#e_addr}" - act "new addresses found: ${_new}" - return $? - } - - [[ -r "$_arg" ]] && { - notice "Import address list from stdin into group file $arg" - act "parsing entries in group file" - _group=`cat $arg` - for i in ${(f)_group}; do - [[ $global_quit = 1 ]] && break + e_addr=() + + # read_stdin + # _stdin=`cat` # reads into var _stdin + for i in "${(f)$(cat)}"; do + [[ $global_quit = 1 ]] && break - # skip comments starting with # - [[ "$i[1]" = "#" ]] && continue - - _parsed=`print - "From: $i" | ${WORKDIR}/bin/fetchaddr -a -x from` - _e="${_parsed[(ws:,:)1]:l}" - - # check if is really an email - isemail "$_e" - [[ $? = 0 ]] || { - func "not an email: $_e" - continue - } + # skip comments starting with # + [[ "$i[1]" = "#" ]] && continue - # check if the email is a duplicate - [[ "${result[$_e]}" = "" ]] || { - func "duplicate email: $_e" - continue - } - - _n="${_parsed[(ws:,:)2]}" - result+=("$_e" "$_n") - done + print - "From: $i" | e_parse From + [[ $? = 0 ]] || continue - # imported all group file contents in results - notice "${#result} entries parsed in $arg" - act "reading entries from stdin, printing out new entries" - - _stdin=`cat` - _new=0 - for i in ${(f)_stdin}; do - [[ $global_quit = 1 ]] && break + _e="${e_parsed[(ws:,:)1]:l}" - # skip comments starting with # - [[ "$i[1]" = "#" ]] && continue - - _parsed=`print - "From: $i" | ${WORKDIR}/bin/fetchaddr -a -x from` - _e="${_parsed[(ws:,:)1]:l}" - - # check if is really an email - isemail "$_e" - [[ $? = 0 ]] || { - func "not an email: $_e" - continue - } - - # check if the email is a duplicate - [[ "${result[$_e]}" = "" ]] || { - func "duplicate email: $_e" - continue - } - - _n="${_parsed[(ws:,:)2]}" - result+=("$_e" "$_n") + # check if the email is not already known + lookup_email "$_e" + [[ $? = 0 ]] && { + func "email already known: $_e" + continue + } - print - "$_n <$_e>" - _new=$(( $_new + 1 )) - done - # TODO: overwrite group file with all entries - notice "Valid unique entries parsed: ${#result}" - act "new addresses found: ${_new}" + _n="${e_parsed[(ws:,:)2]}" - } + print - "$_n <$_e>" + [[ $DRYRUN = 0 ]] && insert_address "$_e" "$_n" + _new=$(( $_new + 1 )) + done + notice "Valid unique entries parsed: ${#e_addr}" + act "new addresses found: ${_new}" return 0 } @@ -465,12 +393,27 @@ EOF # export addressbook to vcard export_vcard() { - _out=${1:-"$MAILDIRS"/$list.vcf} - abook --convert --informat abook \ - --infile "$ADDRESSBOOK" \ - --outformat gcrd --outfile $_out + fn export_vcard $* + + _format=${1:-vcard} + + notice "Reading from stdin addresses to export to format $_format" + c=0 + ztmp + for i in "${(f)$(cat)}"; do + print "alias $i" >> $ztmpfile + c=$(( $c + 1 )) + done + + + abook --convert --informat mutt \ + --infile "$ztmpfile" \ + --outformat $_format + _res=$? - [[ $_res = 0 ]] && notice "$list addressbook exported to VCard file $_out" + + [[ $_res = 0 ]] && { + notice "$c addresses converted to $_format" } return $_res } diff --git a/src/zlibs/email b/src/zlibs/email @@ -38,9 +38,7 @@ compose() { fi done - sysread -t 0 _stdin - - print $_stdin | _mutt ${=_files} -- ${=_addrs} + cat | _mutt ${=_files} -- ${=_addrs} # _mutt -H <(print "To: ${PARAM[@]}") return $? } diff --git a/src/zlibs/helpers b/src/zlibs/helpers @@ -79,22 +79,23 @@ s/"/\&quot;/g # move an email file from a maildir to another # keeping cur/new/tmp positioning refile() { + fn refile $* src="$1" dst="$2" - - [[ "$2" = "" ]] && { + req=(src dst) + ckreq || { error "refile() needs 2 args: source file and destination maildir" return 1 } - [[ -r "$1" ]] || { - error "refile origin file not existing: $1" + [[ -r "$src" ]] || { + error "refile origin file not existing: $src" return 1 } # weak destination check, called should use maildircheck anyway - [[ -d "$2"/new ]] || { - error "refile destination not a maildir: $2" + [[ -d "$dst"/new ]] || { + error "refile destination not a maildir: $dst" return 1 } @@ -105,56 +106,6 @@ refile() { [[ $DRYRUN = 0 ]] && mv $src ${dst}/${pos}/ } -autostart() { - # no argument passed. open first folder with new mail - [[ "$1" = "" ]] && { - - [[ -r $MAILDIRS/.mutt/rc ]] || { - error "Jaro Mail is not yet configured." - error "To configure, edit the files in $MAILDIRS/Accounts" - error "Then run 'jaro update' at least once." - return 1 - } - - _mutt -Z - return $? - } - - # argument passed: determine if an email - isemail "$1" - [[ $? = 0 ]] && { - notice "Composing message to: ${@}" - # its an email, TODO see if we have it in our addressbook - _mutt ${=@} - return 0 - } - - # or a directory of file - [[ -r "$1" ]] && { - # is it a maildir? then open - maildircheck ${1} - [[ $? = 0 ]] && { - _mutt -f ${1} - return 0 - } - # is it a regular file? then attach it - [[ -f "$1" ]] && { - _mutt -a ${=@} - return 0 - } - } - - # or the name of a folder in Jaro Mail - maildircheck "$MAILDIRS/$1" - [[ $? = 0 ]] && { - notice "Opening folder ${1}" - _mutt -f "$MAILDIRS/${1}" - return 0 - } - - return 1 -} - e_parse() { _arg="" # optional second argument limits parsing to header fields diff --git a/src/zlibs/parse b/src/zlibs/parse @@ -22,24 +22,26 @@ + # extract all addresses found in a list of email files from stdin extract_mails() { - _mails=`cat` + mailpaths=( ${=stdin} ) + _tot=${#mailpaths} - _tot=`print $_mails | wc -l` act "$_tot emails to parse" - [[ $_tot -gt 100 ]] && { + + [[ ${_tot} -gt 100 ]] && { act "operation will take a while, showing progress" _prog=0 c=0 } # learn from senders, recipients or all - _action="$1" + _action=${1:-all} _found=0 - for m in ${(f)_mails}; do + for m in ${mailpaths}; do # e_parse fills in e_addr(map) and e_parsed(newline term str) hdr $m | e_parse $_action @@ -83,68 +85,62 @@ extract_maildir() { # search symlinks _mails+=`find $md -type l` - print - ${_mails} | extract_mails "$_action" + stdin="$_mails"; extract_mails "$_action" + return 0 } -# Extract all entries found in stdin. Supports two formats (autodetected) -# 1) list of complete paths to filenames as returned by search -# 2) mbox format big file with special jaromail separator as produced by mutt tagging -extract_stdin() { - func "extract_stdin()" +read_stdin() { # fills global stdin + fn read_stdin - _in=`cat` - # take first line - for i in ${(f)_in}; do _first="$i"; break; done + stdin="" + stdin=`cat` + bytesread=${#stdin} + _res=$? - if [[ "${_first[(w)1]}" = "Date:" ]]; then - # is an email or stream of emails + func "read ${bytesread} bytes from stdin" - _headers=`print - $_in | awk ' -BEGIN { header=1 } -/JAROMAIL_PIPE_SEPARATOR/ { header=1; next } -/^$/ { header=0; print "\n" } -{ if(header==1) { print $0 } } -'` - e_addr=() - _nextline=0 - _gotit="" + # hard limit: read in max 10MB + # sysread -t 10 -c bytesread -s 10000000 stdin + # case $_res in + # 1) warning "read_stdin: there was an error in the parameters to the command." ;; + # 2) warning "read_stdin: there was an error on the read, or on polling the input file descriptor for a timeout." ;; + # 4) warning "read_stdin: the attempt to read timed out." ;; + # 5) warning "read_stdin: no system error occurred, but zero bytes were read." ;; + # 0) return 0 ;; + # esac + +} - for h in ${(f)_headers}; do +stdin_is_muttpipe() { + fn stdin_is_muttpipe + req=(stdin) + ckreq || return 1 - [[ "${h[(w)1]}" = "From:" ]] && _nextline=1 - [[ "${h[(w)1]}" = "Subject:" ]] && { - _nextline=0 - print - ${_gotit} | e_parse + if [[ "${stdin[(w)1]}" = "Date:" ]]; then + return 0 + else + return 1 + fi +} - _gotit="" - } - [[ $_nextline = 1 ]] && _gotit+="$h\n" +stdin_is_pathlist() { + fn stdin_is_pathlist + req=(stdin) + ckreq || return 1 - done - - for i in ${(k)e_addr}; do - print - "${e_addr[$i]} <$i>" - done - - - elif [[ -r "$_first" ]]; then - notice "Parsing ${PARAM} emails addresses from stdin list of files" - # is a list of files - extract_mails ${PARAM} - _res=$? + if [[ -r "${stdin[(w)1]}" ]]; then + return 0 else - error "Cannot process stream from stdin, unknown format" return 1 fi - return $_res } # extract all entries in addressbook or all addresses in a pgp keyring # or all signatures on a pgp key (even without importing it) -extract_auto() { - func "extract() $PARAM" +extract_addresses() { + func "extract_addresses() $PARAM" # without arguments just list all entries in the active list # default is whitelist @@ -154,7 +150,58 @@ extract_auto() { func "extract() arg: $arg (param: $PARAM)" # no arg means parse from stdin - [[ "$arg" = "" ]] && { extract_stdin; return $? } + [[ "$arg" = "" ]] && { + read_stdin + + # Extract all entries found in stdin. Supports two formats (autodetected) + # 1) list of complete paths to filenames as returned by search + # 2) mbox format big file with special jaromail separator as produced by mutt tagging + + # take first word + if stdin_is_muttpipe; then + + act "stdin seems an email or stream of emails" + + _headers=`print - $stdin | awk ' +BEGIN { header=1 } +/JAROMAIL_PIPE_SEPARATOR/ { header=1; next } +/^$/ { header=0; print "\n" } +{ if(header==1) { print $0 } } +'` + + e_addr=() + _nextline=0 + _gotit="" + + for h in ${(f)_headers}; do + + [[ "${h[(w)1]}" = "From:" ]] && _nextline=1 + [[ "${h[(w)1]}" = "Subject:" ]] && { + _nextline=0 + print - ${_gotit} | e_parse + + _gotit="" + } + [[ $_nextline = 1 ]] && _gotit+="$h\n" + + done + + for i in ${(k)e_addr}; do + print - "${e_addr[$i]} <$i>" + done + + + elif stdin_is_pathlist; then + act "stdin seems a stream of full paths to single email files inside maildirs" + # is a list of files + extract_mails ${=PARAM} + _res=$? + else + error "Cannot process stream from stdin, unknown format" + return 1 + fi + return $_res + } [[ -r "$arg" ]] && { # if first arg is a file, could be a maildir, a gpg keyring, @@ -169,13 +216,14 @@ extract_auto() { func "testing argument with file magic" _magic=`file "$arg"` + _recognized=0 # a map to eliminate duplicates - typeset -AU result + typeset -A result ######### GPG # first arg is a GnuPG key ring - [[ "$_magic" =~ "GPG key public ring" ]] && { + [[ "$_magic" =~ "GPG key public ring" ]] && { _recognized=1 notice "Extracting addresses found in GPG keyring: $arg" _addrs=`gpg --list-keys --with-colons | awk -F: '{print $10}'` for i in ${(f)_addrs}; do @@ -191,24 +239,10 @@ extract_auto() { print - "$_n <$_e>" } done - - notice "Unique addresses found: ${#result}" - act "calculating known and new addresses..." - # counts which addresses are known to us - _known=0 - for i in ${(k)result}; do - [[ $global_quit = 1 ]] && break - - lookup_email ${i} - [[ $? = 0 ]] || { - _known=$(( $_known + 1 )) } - done - act "new addresses: $_known" - return 0 } # first arg is a GnuPG public key - [[ "$_magic" =~ "PGP public key" ]] && { + [[ "$_magic" =~ "PGP public key" ]] && { _recognized=1 notice "Extracting addresses from sigs on GPG key $arg" _gpg="gpg --no-default-keyring --keyring $MAILDIRS/cache/pubkey.gpg --batch --with-colons" ${=rm} $MAILDIRS/cache/pubkey.gpg @@ -239,23 +273,9 @@ extract_auto() { print - "$_n <$_e>" } done - - notice "Unique addresses found: ${#result}" - act "calculating known and new addresses..." - # counts which addresses are known to us - _known=0 - for i in ${(k)result}; do - [[ $global_quit = 1 ]] && break - - lookup_email ${i} - [[ $? = 0 ]] || { - _known=$(( $_known + 1 )) } - done - act "new addresses: $_known" - return 0 } - [[ "$_magic" =~ "vCard" ]] && { + [[ "$_magic" =~ "vCard" ]] && { _recognized=1 # parse the vcard and print a simple name and email list # each value on a single line, entry tuples followed by a # # we skip entries that don't have an email @@ -309,29 +329,48 @@ BEGIN { newcard=0; c=0; name=""; email=""; } fi done - - notice "Unique addresses found: ${#result}" - act "calculating known and new addresses..." - # counts which addresses are known to us - _known=0 - for i in ${(k)result}; do - [[ $global_quit = 1 ]] && break - - lookup_email ${i} - [[ $? = 0 ]] || { - _known=$(( $_known + 1 )) } - done - act "new addresses: $_known" - return 0 - } + } + + [[ $_recognized = 1 ]] && { + notice "Unique addresses found: ${#result}" + # act "calculating known and new addresses..." + # # counts which addresses are known to us + # _known=0 + # for i in ${(k)result}; do + # [[ $global_quit = 1 ]] && break + + # lookup_email ${i} + # [[ $? = 0 ]] || { + # _known=$(( $_known + 1 )) } + # done + # act "new addresses: $_known" + return 0 + } } # closes condition in which arg is a file + # final fallback # if no file is recognized, use string as search query - notice "Extracting addresses from search query: $PARAM" - - # run a search and list email files - nm_search ${=PARAM} | extract_mails + error "cannot extract any address from $PARAM" + [[ "$_magic" = "" ]] || { + error "file format not supported: ${_magic[(ws@:@)2]}" } + return 1 } + +extract_headers() { + fn extract_headers + # use cat directly, faster than read_stdin + for i in `cat`; do + [[ -r "$i" ]] || { + warning "cannot extract headers, not a file: $i" } + _folder=${i[(ws:/:)-3]} + hdr $i | awk -v folder=$_folder ' +BEGIN { date=""; from=""; subj="" } +/^From:/ { from=$NF } +/^Date:/ { date=sprintf("%02d %s %s", $3, $4, $5)} +/^Subject:/ { subj=$0} +END { printf("%s :%s: %s\t%s\n", date, folder, from, subj) }' + done +} diff --git a/src/zlibs/search b/src/zlibs/search @@ -42,6 +42,8 @@ nm() { } nm_setup() { + fn nm_setup + nm_dir="$MAILDIRS"/cache/notmuch mkdir -p $nm_dir @@ -85,6 +87,8 @@ EOF } nm_index() { + fn nm_index + # init the environment read_account notice "Indexing all mail archive" @@ -123,6 +127,8 @@ search() { # run a search with notmuch and show results with alot alot_search() { + fn alot_search $* + read_account nm_setup