From 684f9a4812cd209b598d1ce76d5613076c55890f Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 5 Jul 2019 06:12:05 +0200 Subject: typofix --- doc/build.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/build.md b/doc/build.md index 38d1bfb..681f143 100644 --- a/doc/build.md +++ b/doc/build.md @@ -78,12 +78,12 @@ The `doc` target generates all documentation, manpages as well as HTML pages. -Build custom Debian package -=========================== +Build custom Debian packages +============================ Debian GNU/Linux users can also use [`gbp`(1)] from [`git-buildpackage`](https://tracker.debian.org/pkg/git-buildpackage) in -order to build their own package: +order to build their own packages: $ git checkout debian $ gbp buildpackage -- cgit v1.2.3 From 3811a6846c0174c1c91730844ce8ef98627508cc Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 5 Jul 2019 06:04:26 +0200 Subject: Use mailto: links for names of copyright holders. --- doc/build.md | 2 +- doc/development.md | 2 +- doc/index.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/build.md b/doc/build.md index 681f143..ec87e73 100644 --- a/doc/build.md +++ b/doc/build.md @@ -1,5 +1,5 @@ % Build instructions -% Guilhem Moulin +% [Guilhem Moulin](mailto:guilhem@fripost.org) On Debian 9 (codename *Stretch*) and later, installing [`interimap`(1)] is a single command away: diff --git a/doc/development.md b/doc/development.md index 406207a..87eb32c 100644 --- a/doc/development.md +++ b/doc/development.md @@ -1,5 +1,5 @@ % Test environment setup for [`interimap`(1)] and [`pullimap`(1)] -% Guilhem Moulin +% [Guilhem Moulin](mailto:guilhem@fripost.org) Introduction ============ diff --git a/doc/index.md b/doc/index.md index 12de956..281317e 100644 --- a/doc/index.md +++ b/doc/index.md @@ -1,5 +1,5 @@ % [`interimap`(1)] and [`pullimap`(1)] documentation -% Guilhem Moulin +% [Guilhem Moulin](mailto:guilhem@fripost.org) Manuals (HTML versions) ----------------------- -- cgit v1.2.3 From 21d6363c406bd0301b52711a30f98e1f03d0afaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20D=C3=A9trez?= Date: Fri, 5 Jul 2019 15:25:29 +0200 Subject: doc/template.html: Add parent links at the top. --- Makefile | 2 ++ doc/template.html | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index fda0fe0..23a6233 100644 --- a/Makefile +++ b/Makefile @@ -46,8 +46,10 @@ html: $(HTML_FILES) $(HTML_ROOTDIR)/%.html: ./doc/%.md $(HTML_TEMPLATE) mtime="$$(git --no-pager log -1 --pretty="format:%ct" -- "$<" 2>/dev/null)"; \ [ -n "$$mtime" ] || mtime="$$(date +%s -r "$<")"; \ + [ "$<" = "doc/index.md" ] && parent="" || parent="./index.html"; \ pandoc -sp -f markdown -t html+smart --css=$(CSS) --template=$(HTML_TEMPLATE) \ --variable=date:"$$(LC_TIME=C date +"Last modified on %a, %d %b %Y at %T %z" -d @"$$mtime")" \ + --variable=parent:"$$parent" \ --output="$@" -- "$<" doc: manual html diff --git a/doc/template.html b/doc/template.html index e17f0e3..ceb2576 100644 --- a/doc/template.html +++ b/doc/template.html @@ -19,6 +19,12 @@ $endif$ span.smallcaps{font-variant: small-caps;} span.underline{text-decoration: underline;} div.column{display: inline-block; vertical-align: top; width: 50%;} + @media only screen and (min-width: 600px) { + .parent { + float: right; + margin-left: 1em; + } + } $if(quotes)$ q { quotes: "“" "”" "‘" "’"; } $endif$ @@ -48,14 +54,18 @@ $endfor$ $for(include-before)$ $include-before$ $endfor$ -
+
$if(title)$ - + $endif$ $body$ -

@@ -72,5 +82,6 @@ $endif$
+ -- cgit v1.2.3 From 420cd08692d4962155b23925eabc74d9002bf55d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 5 Jul 2019 15:29:07 +0200 Subject: doc/template.html: Fix minor space damage. --- doc/template.html | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/doc/template.html b/doc/template.html index ceb2576..17cbdbe 100644 --- a/doc/template.html +++ b/doc/template.html @@ -15,18 +15,21 @@ $if(keywords)$ $endif$ $if(title-prefix)$$title-prefix$ – $endif$$pagetitle$ $if(highlighting-css)$ @@ -37,11 +40,6 @@ $endif$ $for(css)$ $endfor$ - $if(math)$ $math$ $endif$ -- cgit v1.2.3 From 95659a29d2cb47192afa00174ca65e92b1bdf217 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 5 Jul 2019 15:30:34 +0200 Subject: doc/*.html: Add 'keywords' and 'lang' tags. --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 23a6233..4fc759f 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,8 @@ $(HTML_ROOTDIR)/%.html: ./doc/%.md $(HTML_TEMPLATE) [ "$<" = "doc/index.md" ] && parent="" || parent="./index.html"; \ pandoc -sp -f markdown -t html+smart --css=$(CSS) --template=$(HTML_TEMPLATE) \ --variable=date:"$$(LC_TIME=C date +"Last modified on %a, %d %b %Y at %T %z" -d @"$$mtime")" \ + --variable=keywords:"interimap" \ + --variable=lang:"en" \ --variable=parent:"$$parent" \ --output="$@" -- "$<" -- cgit v1.2.3 From 060554e73c9effe783a10c25e83df80c5e7a33ea Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 5 Jul 2019 15:31:31 +0200 Subject: doc/*.md: Improve wording. --- doc/development.md | 2 +- doc/index.md | 2 +- doc/template.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/development.md b/doc/development.md index 87eb32c..428c11d 100644 --- a/doc/development.md +++ b/doc/development.md @@ -1,4 +1,4 @@ -% Test environment setup for [`interimap`(1)] and [`pullimap`(1)] +% Test environment setup % [Guilhem Moulin](mailto:guilhem@fripost.org) Introduction diff --git a/doc/index.md b/doc/index.md index 281317e..a403a3e 100644 --- a/doc/index.md +++ b/doc/index.md @@ -1,4 +1,4 @@ -% [`interimap`(1)] and [`pullimap`(1)] documentation +% [`interimap`(1)] and [`pullimap`(1)] % [Guilhem Moulin](mailto:guilhem@fripost.org) Manuals (HTML versions) diff --git a/doc/template.html b/doc/template.html index 17cbdbe..97062e3 100644 --- a/doc/template.html +++ b/doc/template.html @@ -70,7 +70,7 @@ $body$
$if(author)$ - © + Copyright © $for(author)$$author$$sep$, $endfor$ $endif$
-- cgit v1.2.3 From 955cda2cfa32ca6e475b6726df4732f5dc98a30f Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 5 Jul 2019 17:39:46 +0200 Subject: doc/template.html: Justify paragraphs on larger screens. --- doc/template.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/template.html b/doc/template.html index 97062e3..a3a3938 100644 --- a/doc/template.html +++ b/doc/template.html @@ -24,6 +24,9 @@ $endif$ float: right; margin-left: 1em; } + .content p { + text-align: justify; + } } @media(max-width: 1440px) { .container{ max-width: 1080px; } } @media(max-width: 1280px) { .container{ max-width: 960px; } } @@ -64,6 +67,7 @@ $endif$ $endif$ $body$ +

@@ -80,6 +84,5 @@ $endif$
- -- cgit v1.2.3 From 4b61d5e13773bf8bf25537f28d1a42f7c6473b75 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 6 Jul 2019 14:55:04 +0200 Subject: doc/*: Fix minor space damage. Also, set tab size to 4 spaces in the HTML for consistency. --- doc/build.md | 10 +++++----- doc/development.md | 14 +++++++------- doc/template.html | 1 + 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/doc/build.md b/doc/build.md index ec87e73..5c362f1 100644 --- a/doc/build.md +++ b/doc/build.md @@ -33,9 +33,9 @@ On Debian GNU/Linux systems, the dependencies can be installed with the following command: $ apt install libconfig-tiny-perl \ - libdbi-perl \ - libdbd-sqlite3-perl \ - libnet-ssleay-perl + libdbi-perl \ + libdbd-sqlite3-perl \ + libnet-ssleay-perl Additional packages are required in order to run the test suite: @@ -68,8 +68,8 @@ the `CSS` environment variable (the value of which defaults to For instance, use $ CSS="https://guilhem.org/static/css/bootstrap.min.css" \ - HTML_ROOTDIR="$XDG_RUNTIME_DIR/interimap" \ - make html + HTML_ROOTDIR="$XDG_RUNTIME_DIR/interimap" \ + make html to generate the HTML documentation under directory `$XDG_RUNTIME_DIR/interimap` (which needs to exist) using a remote CSS file. diff --git a/doc/development.md b/doc/development.md index 428c11d..49e8d74 100644 --- a/doc/development.md +++ b/doc/development.md @@ -83,7 +83,7 @@ pre-authenticated [IMAP4rev1] in the test environment for username `testuser`, list mailboxes, and exit, run: $ env -i PATH="/usr/bin:/bin" USER="testuser" \ - doveadm -c "$BASEDIR/dovecot.conf" exec imap + doveadm -c "$BASEDIR/dovecot.conf" exec imap * PREAUTH [CAPABILITY IMAP4rev1 …] Logged in as testuser a LIST "" "*" * LIST (\HasNoChildren) "." INBOX @@ -98,10 +98,10 @@ the latter to create a mailbox `foo`, add a sample message to it, and finally mark it as `\Seen`. $ env -i PATH="/usr/bin:/bin" USER="testuser" \ - doveadm -c "$BASEDIR/dovecot.conf" mailbox create "foo" + doveadm -c "$BASEDIR/dovecot.conf" mailbox create "foo" $ env -i PATH="/usr/bin:/bin" USER="testuser" HOME="$BASEDIR/testuser" \ - doveadm -c "$BASEDIR/dovecot.conf" exec dovecot-lda -e -m "foo" <<-EOF + doveadm -c "$BASEDIR/dovecot.conf" exec dovecot-lda -e -m "foo" <<-EOF From: To: Subject: Hello world! @@ -112,7 +112,7 @@ finally mark it as `\Seen`. EOF $ env -i PATH="/usr/bin:/bin" USER="testuser" \ - doveadm -c "$BASEDIR/dovecot.conf" flags add "\\Seen" mailbox "foo" "*" + doveadm -c "$BASEDIR/dovecot.conf" flags add "\\Seen" mailbox "foo" "*" Normally [`dovecot-lda`(1)](https://wiki.dovecot.org/LDA) tries to do a userdb lookup in order to determine the user's home directory. Since we @@ -155,7 +155,7 @@ You can now run [`interimap`(1)] with `--watch` set, here to one second to observe synchronisation steps early. $ env -i PATH="$PATH" perl -I./lib -T ./interimap --config="$BASEDIR/interimap.conf" \ - --watch=1 --debug + --watch=1 --debug Use instructions from the [previous section][Mail storage access] (substituting `testuser` with `local` or `remote`) in order to simulate @@ -179,12 +179,12 @@ Create a [`pullimap`(1)] configuration file with as section `[foo]`. Run [`pullimap`(1)] without `--idle` in order to create the state file. $ env -i PATH="$PATH" perl -I./lib -T ./pullimap --config="$BASEDIR/pullimap.conf" \ - --no-delivery foo + --no-delivery foo You can now run [`pullimap`(1)] with `--idle` set. $ env -i PATH="$PATH" perl -I./lib -T ./pullimap --config="$BASEDIR/pullimap.conf" \ - --no-delivery --idle --debug foo + --no-delivery --idle --debug foo Use instructions from the [previous section][Mail storage access] in order to simulate activity on the “remote” server (in the relevant diff --git a/doc/template.html b/doc/template.html index a3a3938..2cd7cc9 100644 --- a/doc/template.html +++ b/doc/template.html @@ -19,6 +19,7 @@ $endif$ span.smallcaps{font-variant: small-caps;} span.underline{text-decoration: underline;} div.column{display: inline-block; vertical-align: top; width: 50%;} + pre{tab-size: 4; -moz-tab-size: 4;} @media only screen and (min-width: 600px) { .parent { float: right; -- cgit v1.2.3 From 550f778dbcb84a9aa67732b1fff0191b55bea24c Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 6 Jul 2019 19:50:06 +0200 Subject: interimap: clarify that 'ignore-mailbox' is matched against internal names. That is, without leading reference, and where the hierarchy delimiter is replaced with null characters. /!\ This changes breaks backward compatibility! --- Changelog | 8 ++++++++ doc/interimap.1.md | 5 ++++- interimap.sample | 9 +++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index 4cc66ba..84a62b6 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,13 @@ interimap (0.5) upstream; + Breaking changes: + * interimap: when matching mailbox names against the 'ignore-mailbox' + pattern, the hierarchy delimiter is substituted with a null character + before hand. For instance one should now use '^virtual(?:\x00|$)' to + exclude the mailbox named 'virtual' as well as its descendants + (regardless of the hierarchy delimiter in use). + + Other changes: * interimap: the space-speparated list of names and/or patterns in 'list-mailbox' can now contain C-style escape sequences (backslash and hexadecimal escape). diff --git a/doc/interimap.1.md b/doc/interimap.1.md index 387850a..d7c3711 100644 --- a/doc/interimap.1.md +++ b/doc/interimap.1.md @@ -266,7 +266,10 @@ Valid options are: : An optional Perl Compatible Regular Expressions ([PCRE]) covering mailboxes to exclude: any ([UTF-7 encoded][RFC 2152] and unquoted) mailbox listed in the initial `LIST` responses is ignored if it - matches the given expression. + matches the given expression after trimming the reference names and + substituting the hiearchy delimiter with the null character. For + instance, specifying `^virtual(?:\x00|$)` excludes the mailbox named + “virtual” as well as its descendants. Note that the *MAILBOX*es given as command-line arguments bypass the check and are always considered for synchronization. This option is only available in the default section. diff --git a/interimap.sample b/interimap.sample index f771e54..2433563 100644 --- a/interimap.sample +++ b/interimap.sample @@ -1,7 +1,12 @@ #database = imap.example.org.db -#list-mailbox = "*" + +# only consider subscribed mailboxes list-select-opts = SUBSCRIBED -ignore-mailbox = ^virtual/ +#list-mailbox = "*" + +# ignore the mailbox named 'virtual' and its descendants +ignore-mailbox = ^virtual(?:\x00|$) + [local] type = tunnel -- cgit v1.2.3 From 17ecce0dd72fd3b857210fbff3f356afc9ba0f75 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 7 Jul 2019 03:45:35 +0200 Subject: interimap.1: Clarify handling of delimiter in mailbox names. --- doc/interimap.1.md | 13 +++++++++++-- interimap | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/interimap.1.md b/doc/interimap.1.md index d7c3711..b7707af 100644 --- a/doc/interimap.1.md +++ b/doc/interimap.1.md @@ -85,7 +85,10 @@ However if some extra argument are provided on the command line, `interimap` ignores these options and synchronizes the given *MAILBOX*es instead. Note that each *MAILBOX* is taken “as is”; in particular, it must be [UTF-7 encoded][RFC 2152], unquoted, and the list -wildcards ‘\*’ and ‘%’ are passed verbatim to the IMAP server. +wildcards ‘\*’ and ‘%’ are passed verbatim to the IMAP server. If the +local and remote hierarchy delimiter differ, then within the *MAILBOX* +names the *local* delimiter should be used (it is transparently +substituted for remote commands and responses). If the synchronization was interrupted during a previous run while some messages were being replicated (but before the `UIDNEXT` or @@ -245,7 +248,13 @@ Valid options are: Two wildcards are available, and passed verbatim to the IMAP server: a ‘\*’ character matches zero or more characters, while a ‘%’ character matches zero or more characters up to the hierarchy - delimiter. + delimiter. Hardcoding the hierarchy delimiter in this setting is + not advised because the server might silently change it at some + point. A null character should be used instead. For instance, if + *list-mailbox* is set `"foo\x00bar"` then, assuming the hierarchy + delimiter is ‘/’, only the mailbox named `foo/bar` is considered for + synchronization. + This option is only available in the default section. (The default pattern, `*`, matches all visible mailboxes on the server.) diff --git a/interimap b/interimap index 7054f88..ab96c9c 100755 --- a/interimap +++ b/interimap @@ -280,7 +280,7 @@ sub list_mailboxes($) { # INBOX exists in a namespace of its own, so it may have a different separator. # All other mailboxes MUST have the same separator though, per 3501 sec. 7.2.2 - # and https://www.imapwiki.org/ClientImplementation/MailboxList#Hierarchy_separators + # and https://imapwiki.org/ClientImplementation/MailboxList#Hierarchy_separators # (We assume all list-mailbox arguments given live in the same namespace. Otherwise # the user needs to start multiple interimap instances.) delete $delims->{INBOX}; -- cgit v1.2.3 From 39faf86e122fefe4a8093f3b6609658c56c696c0 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 8 Jul 2019 05:30:37 +0200 Subject: libinterimap: use directories relative to $HOME for the XDG defaults. Previously getpwuid() was called to determine the user's home directory, while the XDG specification explicitely mentions $HOME. Conveniently our docs always mentioned ~/, which on POSIX-compliant systems expands to the value of the variable HOME (and the result is unspecified when the variable is unset). Cf. Shell and Utilities volume of POSIX.1-2017, sec. 2.6.1: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_01 --- Changelog | 7 +++++++ lib/Net/IMAP/InterIMAP.pm | 6 ++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Changelog b/Changelog index 84a62b6..d429932 100644 --- a/Changelog +++ b/Changelog @@ -69,6 +69,13 @@ interimap (0.5) upstream; the 'foreign_keys' PRAGMA during a transaction is a documented no-op). - interimap: fix handling of mod-sequence values greater or equal than 2 << 63. + - libinterimap: use directories relative to $HOME for the XDG + environment variables default values. Previously getpwuid() was + called to determine the user's home directory, while the XDG + specification explicitely mentions $HOME. Conveniently our docs + always mentioned ~/, which on POSIX-compliant systems expands to the + value of the variable HOME. (Cf. Shell and Utilities volume of + POSIX.1-2017, sec. 2.6.1.) -- Guilhem Moulin Fri, 10 May 2019 00:58:14 +0200 diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index 19895c4..aacc8e7 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -92,11 +92,9 @@ sub xdg_basedir($$$$) { return $path if $path =~ /\A\//; my $basedir = $ENV{$xdg_variable}; - unless (defined $basedir) { - my @getent = getpwuid($>); - $basedir = $getent[7] ."/". $default; - } + $basedir = ($ENV{HOME} // "") ."/". $default unless defined $basedir; die "No such directory: ", $basedir unless -d $basedir; + $basedir .= "/".$subdir; $basedir =~ /\A(\/\p{Print}+)\z/ or die "Insecure $basedir"; $basedir = $1; -- cgit v1.2.3 From 67e0d741f21bd589a2cbb4d23f07f5fb5eae889b Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 28 Aug 2019 10:58:59 +0200 Subject: typofix --- doc/interimap.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/interimap.1.md b/doc/interimap.1.md index b7707af..0fb83ea 100644 --- a/doc/interimap.1.md +++ b/doc/interimap.1.md @@ -276,7 +276,7 @@ Valid options are: mailboxes to exclude: any ([UTF-7 encoded][RFC 2152] and unquoted) mailbox listed in the initial `LIST` responses is ignored if it matches the given expression after trimming the reference names and - substituting the hiearchy delimiter with the null character. For + substituting the hierarchy delimiter with the null character. For instance, specifying `^virtual(?:\x00|$)` excludes the mailbox named “virtual” as well as its descendants. Note that the *MAILBOX*es given as command-line arguments bypass the -- cgit v1.2.3 From 2f8350700091e766bdab24e7e8d8e051701da9e2 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 6 Nov 2019 02:55:18 +0100 Subject: pullimap, interimap: redact AUTHENTICATE and LOGIN commands In --debug mode in order to avoid inadvertently receiving credentials in bug reports. --debug can be set twice to spell out these commands in full. --- Changelog | 4 ++++ doc/interimap.1.md | 9 +++++---- doc/pullimap.1.md | 9 +++++---- interimap | 2 +- lib/Net/IMAP/InterIMAP.pm | 17 +++++++++++++++-- pullimap | 2 +- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/Changelog b/Changelog index d429932..9f718cd 100644 --- a/Changelog +++ b/Changelog @@ -44,6 +44,10 @@ interimap (0.5) upstream; is run following Perl's `exec` semantics: it is passed to `/bin/sh -c` when it contains shell metacharacters; and split into words and passed to execvp(3) otherwise. + + interimap, pullimap: redact AUTHENTICATE and LOGIN commands in + --debug mode in order to avoid inadvertently receiving credentials in + bug reports. --debug can be set twice to spell out these commands in + full. - libinterimap: bugfix: hierarchy delimiters in LIST responses were returned as an escaped quoted special, like "\\", not as a single character (backslash in this case). diff --git a/doc/interimap.1.md b/doc/interimap.1.md index 0fb83ea..8fa5def 100644 --- a/doc/interimap.1.md +++ b/doc/interimap.1.md @@ -178,10 +178,11 @@ Options `--debug` -: Turn on debug mode. Debug messages are written to the given *logfile*. - Note that this include all IMAP traffic (except literals). - Depending on the chosen authentication mechanism, this might include - authentication credentials. +: Turn on debug mode. Debug messages, which includes all IMAP traffic + besides literals, are written to the given *logfile*. The `LOGIN` + and `AUTHENTICATE` commands are however redacted (in order to avoid + disclosing authentication credentials) unless the `--debug` flag is + set multiple times. `-h`, `--help` diff --git a/doc/pullimap.1.md b/doc/pullimap.1.md index 1b2e509..d40ece8 100644 --- a/doc/pullimap.1.md +++ b/doc/pullimap.1.md @@ -57,10 +57,11 @@ Options `--debug` -: Turn on debug mode. Debug messages are written to the error output. - Note that this include all IMAP traffic (except literals). - Depending on the chosen authentication mechanism, this might include - authentication credentials. +: Turn on debug mode. Debug messages, which includes all IMAP traffic + besides literals, are written to the given *logfile*. The `LOGIN` + and `AUTHENTICATE` commands are however redacted (in order to avoid + disclosing authentication credentials) unless the `--debug` flag is + set multiple times. `-h`, `--help` diff --git a/interimap b/interimap index ab96c9c..207d389 100755 --- a/interimap +++ b/interimap @@ -57,7 +57,7 @@ sub usage(;$) { } my @COMMANDS = qw/repair delete rename/; -usage(1) unless GetOptions(\%CONFIG, qw/config=s quiet|q target=s@ debug help|h watch:i notify/, @COMMANDS); +usage(1) unless GetOptions(\%CONFIG, qw/config=s quiet|q target=s@ debug+ help|h watch:i notify/, @COMMANDS); usage(0) if $CONFIG{help}; my $COMMAND = do { my @command = grep {exists $CONFIG{$_}} @COMMANDS; diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index aacc8e7..76135ea 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -436,8 +436,21 @@ sub new($%) { $self->fail("Unsupported authentication mechanism: $mech"); } + my $dbg; delete $self->{password}; # no need to remember passwords + if (($self->{debug} // 0) == 1) { + $dbg = $self->{debug}--; + my $cmd = $command =~ /\A(LOGIN) / ? $1 + : $command =~ /\A(AUTHENTICATE \S+)(?: .*)?\z/ ? $1 + : $self->panic(); + $self->logger('C: xxx ', $cmd, ' [REDACTED]'); + } $self->_send($command, $callback); + if (defined $dbg) { + $self->logger('S: xxx ', $IMAP_text); + $self->{debug} = $dbg; + } + unless ($IMAP_text =~ /\A\Q$IMAP_cond\E \[CAPABILITY /) { # refresh the CAPABILITY list since the previous one had only pre-login capabilities delete $self->{_CAPABILITIES}; @@ -1826,8 +1839,8 @@ sub _cmd_extend($$) { $self->_cmd_extend_($args); } else { - # server supports LITERAL+: flush the command before each - # literal + # server doesn't supports LITERAL+: flush the command before + # each literal my ($offset, $litlen) = (0, 0); while ( (my $idx = index($$args, "\n", $offset+$litlen)) >= 0 ) { my $line = substr($$args, $offset, $idx+1-$offset); diff --git a/pullimap b/pullimap index e1c96e8..a39d420 100755 --- a/pullimap +++ b/pullimap @@ -52,7 +52,7 @@ sub usage(;$) { exit $rv; } -usage(1) unless GetOptions(\%CONFIG, qw/config=s quiet|q debug help|h idle:i no-delivery/); +usage(1) unless GetOptions(\%CONFIG, qw/config=s quiet|q debug+ help|h idle:i no-delivery/); usage(0) if $CONFIG{help}; usage(1) unless $#ARGV == 0 and $ARGV[0] ne '_'; -- cgit v1.2.3 From 1dea617bfa23f09f94270125ff51c0b2b96e39c8 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 6 Nov 2019 03:21:59 +0100 Subject: Allow lowercase SASL mechanisms. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 2222 sec. 3 says that values are “from 1 to 20 characters in length, consisting of upper-case letters, digits, hyphens, and/or underscores” so we always upper-case the value. --- lib/Net/IMAP/InterIMAP.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index 76135ea..afb5694 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -407,8 +407,8 @@ sub new($%) { @caps = $self->capabilities(); } - my @mechs = ('LOGIN', grep defined, map { /^AUTH=(.+)/i ? $1 : undef } @caps); - my $mech = (grep defined, map {my $m = $_; (grep {$m eq $_} @mechs) ? $m : undef} + my @mechs = ('LOGIN', grep defined, map { /^AUTH=(.+)/i ? uc($1) : undef } @caps); + my $mech = (grep defined, map {my $m = uc($_); (grep {$m eq $_} @mechs) ? $m : undef} split(/ /, $self->{auth}))[0]; $self->fail("Failed to choose an authentication mechanism") unless defined $mech; $self->fail("Logins are disabled.") if ($mech eq 'LOGIN' or $mech eq 'PLAIN') and -- cgit v1.2.3 From 990344e026f4248c27f13d4fd59360fb6f03f978 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 6 Nov 2019 03:46:45 +0100 Subject: interimap.sample: don't hardcode Dovecot's libexec dir. --- interimap.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interimap.sample b/interimap.sample index 2433563..b0f4b95 100644 --- a/interimap.sample +++ b/interimap.sample @@ -10,7 +10,7 @@ ignore-mailbox = ^virtual(?:\x00|$) [local] type = tunnel -command = /usr/lib/dovecot/imap +command = exec doveadm exec imap null-stderr = YES [remote] -- cgit v1.2.3 From e764f7517749d8055ba3af8ae00a0c75a3bccaa7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 6 Nov 2019 05:33:34 +0100 Subject: Update copyright years. --- lib/Net/IMAP/InterIMAP.pm | 2 +- pullimap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index afb5694..ff10854 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -1,6 +1,6 @@ #---------------------------------------------------------------------- # A minimal IMAP4 client for QRESYNC-capable servers -# Copyright © 2015-2018 Guilhem Moulin +# Copyright © 2015-2019 Guilhem Moulin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/pullimap b/pullimap index a39d420..81811e9 100755 --- a/pullimap +++ b/pullimap @@ -2,7 +2,7 @@ #---------------------------------------------------------------------- # Pull mails from an IMAP mailbox and deliver them to an SMTP session -# Copyright © 2016-2018 Guilhem Moulin +# Copyright © 2016-2019 Guilhem Moulin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -- cgit v1.2.3 From 30276bbc82ca770500531d872666f48493749285 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 6 Nov 2019 03:46:36 +0100 Subject: interimap.1.md: Document that DELETE and RENAME commands should be avoided. --- doc/interimap.1.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/interimap.1.md b/doc/interimap.1.md index 8fa5def..42db381 100644 --- a/doc/interimap.1.md +++ b/doc/interimap.1.md @@ -437,7 +437,7 @@ Known bugs and limitations empty *database* will duplicate each message due to the absence of local ↔ remote UID association. Hence one needs to manually empty the mail store on one end when migrating to `interimap` from another - synchronisation solution. + synchronization solution. * `interimap` is single threaded and doesn't use IMAP command pipelining. Synchronization could be boosted up by sending @@ -452,6 +452,13 @@ Known bugs and limitations was deleted while another one (which is replicated again) was added to the other mailbox in the meantime. + * Because the [IMAP protocol][RFC 3501] doesn't provide a way for + clients to determine whether a disapeared mailbox was deleted or + renamed, `interimap` aborts when a known mailbox disapeared from one + server but not the other. The `--delete` (resp. `rename`) command + should be used instead to delete (resp. rename) the mailbox on both + servers as well as within `interimap`'s internal database. + * `PLAIN` and `LOGIN` are the only authentication mechanisms currently supported. -- cgit v1.2.3 From ab2774a3039efc0efe0a4bc840675bf6d36435d7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 6 Nov 2019 18:20:04 +0100 Subject: interimap.1.md: Hint to `doveadm-deduplicate` to weed out duplicates. --- doc/interimap.1.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/interimap.1.md b/doc/interimap.1.md index 42db381..d5dd685 100644 --- a/doc/interimap.1.md +++ b/doc/interimap.1.md @@ -435,9 +435,10 @@ Known bugs and limitations * Using `interimap` on two identical servers with a non-existent or empty *database* will duplicate each message due to the absence of - local ↔ remote UID association. Hence one needs to manually empty - the mail store on one end when migrating to `interimap` from another - synchronization solution. + local ↔ remote UID association. (Should they arise, an external tool + such as [`doveadm-deduplicate`(1)] can be used to weed them out.) + Hence one needs to manually empty the mail store on one end when + migrating to `interimap` from another synchronization solution. * `interimap` is single threaded and doesn't use IMAP command pipelining. Synchronization could be boosted up by sending @@ -544,3 +545,4 @@ Standards [PCRE]: https://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions [`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/apps/ciphers.html [`verify`(1ssl)]: https://www.openssl.org/docs/manmaster/apps/verify.html +[`doveadm-deduplicate`(1)]: https://wiki.dovecot.org/Tools/Doveadm/Deduplicate -- cgit v1.2.3 From bf9272b19724c351cd211067afb177c37c87f210 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 7 Nov 2019 03:53:33 +0100 Subject: wibble --- interimap | 17 +++++++++-------- lib/Net/IMAP/InterIMAP.pm | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/interimap b/interimap index 207d389..afe18e9 100755 --- a/interimap +++ b/interimap @@ -88,17 +88,17 @@ my $CONF = do { my ($DBFILE, $LOGGER_FD, %LIST); { - $DBFILE = $CONF->{_}->{database} if defined $CONF->{_}; + $CONF->{_} //= {}; + $DBFILE = $CONF->{_}->{database}; $DBFILE //= $CONF->{remote}->{host}.'.db' if defined $CONF->{remote}; $DBFILE //= $CONF->{local}->{host}. '.db' if defined $CONF->{local}; die "Missing option database" unless defined $DBFILE; $DBFILE = xdg_basedir( XDG_DATA_HOME => ".local/share", $NAME, $DBFILE ); - if (defined $CONF->{_} and defined $CONF->{_}->{logfile}) { + if (defined (my $l = $CONF->{_}->{logfile})) { require 'POSIX.pm'; require 'Time/HiRes.pm'; - open $LOGGER_FD, '>>', $CONF->{_}->{logfile} - or die "Can't open $CONF->{_}->{logfile}: $!\n"; + open $LOGGER_FD, '>>', $l or die "Can't open $l: $!\n"; $LOGGER_FD->autoflush(1); my $flags = fcntl($LOGGER_FD, F_GETFD, 0) or die "fcntl F_GETFD: $!"; fcntl($LOGGER_FD, F_SETFD, $flags | FD_CLOEXEC) or die "fcntl F_SETFD: $!"; @@ -149,7 +149,8 @@ my ($IMAP, $lIMAP, $rIMAP); sub cleanup() { undef $_ foreach grep defined, ($IMAP, $lIMAP, $rIMAP); logger(undef, "Cleaning up...") if $CONFIG{debug}; - close $LOGGER_FD if defined $LOGGER_FD; + $LOGGER_FD->close() if defined $LOGGER_FD and defined $LOGGER_FD->fileno + and $LOGGER_FD->fileno != fileno STDERR; $DBH->disconnect() if defined $DBH; } $SIG{INT} = sub { msg(undef, $!); cleanup(); exit 1; }; @@ -219,8 +220,8 @@ foreach my $name (qw/local remote/) { $config{'compress'} //= ($name eq 'local' ? 0 : 1); $config{keepalive} = 1 if $CONFIG{watch} and $config{type} ne 'tunnel'; - $IMAP->{$name} = { client => Net::IMAP::InterIMAP::->new(%config) }; - my $client = $IMAP->{$name}->{client}; + my $client = Net::IMAP::InterIMAP::->new(%config); + $IMAP->{$name} = { client => $client }; die "Non $_-capable IMAP server.\n" foreach $client->incapable(qw/LIST-EXTENDED UIDPLUS/); die "Non LIST-STATUS-capable IMAP server.\n" if !$CONFIG{notify} and $client->incapable('LIST-STATUS'); @@ -1161,7 +1162,7 @@ sub callback_new_message($$$$;$$$) { } else { # use MULTIAPPEND (RFC 3502) - # proceed by batches of 1MB to save roundtrips without blowing up the memory + # proceed by 1MiB batches to save roundtrips without blowing up the memory if (@$buff and $$bufflen + $length > 1048576) { @UIDs = callback_new_message_flush($idx, $mailbox, $name, @$buff); @$buff = (); diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index ff10854..dd4134d 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -520,6 +520,7 @@ sub stats($) { # Destroy a Net::IMAP::InterIMAP object. sub DESTROY($) { + local($., $@, $!, $^E, $?); my $self = shift; $self->{_STATE} = 'LOGOUT'; -- cgit v1.2.3 From e8c3b83914fe86edfa9c209de64fcc225c820bc7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 7 Nov 2019 20:32:44 +0100 Subject: typofix --- doc/interimap.1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/interimap.1.md b/doc/interimap.1.md index d5dd685..febec28 100644 --- a/doc/interimap.1.md +++ b/doc/interimap.1.md @@ -454,8 +454,8 @@ Known bugs and limitations to the other mailbox in the meantime. * Because the [IMAP protocol][RFC 3501] doesn't provide a way for - clients to determine whether a disapeared mailbox was deleted or - renamed, `interimap` aborts when a known mailbox disapeared from one + clients to determine whether a disappeared mailbox was deleted or + renamed, `interimap` aborts when a known mailbox disappeared from one server but not the other. The `--delete` (resp. `rename`) command should be used instead to delete (resp. rename) the mailbox on both servers as well as within `interimap`'s internal database. -- cgit v1.2.3 From 5b122e3a383c8e7603f1fc2322a6fe5298078a65 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 7 Nov 2019 03:58:29 +0100 Subject: libinterimap: Free reference to $self in _start_ssl(). (We don't need the function anymore once the handshake is established). Otherwise the reference count of that IMAP client never gets to 0 before the global destruction phase. For interimap, this causes traffic stats to be printed not by the cleanup() function as intended, but just before the program exits. --- lib/Net/IMAP/InterIMAP.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index dd4134d..9c95109 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -1690,6 +1690,7 @@ sub _start_ssl($$) { } @$self{qw/_SSL _SSL_CTX/} = ($ssl, $ctx); + undef $self; # the verify callback has reference to $self, free it now } -- cgit v1.2.3 From 6c5f762596af9567afc4691beea212483fa7a07a Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 7 Nov 2019 19:57:34 +0100 Subject: libinterimap: Don't panic at the end of the compressed stream. Cf. Compress::Raw::Zlib's documentation. Z_STREAM_END denotes a successful state. --- Changelog | 2 ++ lib/Net/IMAP/InterIMAP.pm | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index 9f718cd..964fab8 100644 --- a/Changelog +++ b/Changelog @@ -80,6 +80,8 @@ interimap (0.5) upstream; always mentioned ~/, which on POSIX-compliant systems expands to the value of the variable HOME. (Cf. Shell and Utilities volume of POSIX.1-2017, sec. 2.6.1.) + - libinterimap: don't panic() when inflate() reports the end of the + compression stream is reached. -- Guilhem Moulin Fri, 10 May 2019 00:58:14 +0200 diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index 9c95109..2d1f644 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -20,7 +20,7 @@ package Net::IMAP::InterIMAP v0.0.5; use warnings; use strict; -use Compress::Raw::Zlib qw/Z_OK Z_FULL_FLUSH Z_SYNC_FLUSH MAX_WBITS/; +use Compress::Raw::Zlib qw/Z_OK Z_STREAM_END Z_FULL_FLUSH Z_SYNC_FLUSH MAX_WBITS/; use Config::Tiny (); use Errno qw/EEXIST EINTR/; use Net::SSLeay 1.73 (); @@ -1723,8 +1723,9 @@ sub _getline($;$) { $self->{_OUTRAWCOUNT} += $n; if (defined (my $i = $self->{_Z_INFLATE})) { - $i->inflate($buf, $self->{_OUTBUF}) == Z_OK or - $self->panic("Inflation failed: ", $i->msg()); + my $r = $i->inflate($buf, $self->{_OUTBUF}); + $self->panic("Inflation failed: $r ", $i->msg()) + unless $r == Z_OK or $r == Z_STREAM_END; } else { $self->{_OUTBUF} = $buf; -- cgit v1.2.3 From a4a371234215a7705f304875cc8af067bf3142af Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 7 Nov 2019 16:42:52 +0100 Subject: Refactor logging logic. Also, introduce new option 'logger-prefix' to determine the prefix of each log line. Closes: #942725. --- Changelog | 2 + doc/interimap.1.md | 13 +++++ interimap | 109 +++++++++++++++++++------------------- lib/Net/IMAP/InterIMAP.pm | 73 ++++++++++++++++--------- pullimap | 3 +- tests/01-rename-exists-db/run | 2 +- tests/01-rename-exists-local/run | 2 +- tests/01-rename-exists-remote/run | 2 +- tests/03-sync-mailbox-list/run | 4 +- tests/05-repair/run | 6 +-- 10 files changed, 126 insertions(+), 90 deletions(-) diff --git a/Changelog b/Changelog index 964fab8..48481dd 100644 --- a/Changelog +++ b/Changelog @@ -48,6 +48,8 @@ interimap (0.5) upstream; --debug mode in order to avoid inadvertently receiving credentials in bug reports. --debug can be set twice to spell out these commands in full. + + interimap: new option 'log-prefix' to control the prefix of each log + entry, depending on the component name and relevant mailbox. - libinterimap: bugfix: hierarchy delimiters in LIST responses were returned as an escaped quoted special, like "\\", not as a single character (backslash in this case). diff --git a/doc/interimap.1.md b/doc/interimap.1.md index febec28..ee92668 100644 --- a/doc/interimap.1.md +++ b/doc/interimap.1.md @@ -290,6 +290,19 @@ Valid options are: default these messages are written to the error output.) This option is only available in the default section. +*log-prefix* + +: A `printf`(3)-like format string to use as prefix for each log + message. Interpreted sequences are `%n` and `%m`, expanding + respectively to the component name (*local*/*remote*) and to the + name of the mailbox relevant for the log entry. Conditions on a + specifier `%X` can be obtained with `%?X?then?` or `%?X?then&else?`, + which expands to *then* if the `%X` specifier expands to a non-empty + string, and to *else* (or the empty string if there is no else + condition) if it doesn't. Literal `%` characters need to be escaped + as `%%`, while `&`, `?` and `\` characters need to be `\`-escaped. + (Default: `%?n?%?m?%n(%m)&%n?: ?`.) + *type* : One of `imap`, `imaps` or `tunnel`. diff --git a/interimap b/interimap index afe18e9..87c3a64 100755 --- a/interimap +++ b/interimap @@ -79,13 +79,14 @@ my $CONF = do { , [qw/_ local remote/] , database => qr/\A(\P{Control}+)\z/ , logfile => qr/\A(\/\P{Control}+)\z/ + , 'log-prefix' => qr/\A(\P{Control}*)\z/ , 'list-reference' => qr/\A([\x01-\x09\x0B\x0C\x0E-\x7F]*)\z/ , 'list-mailbox' => qr/\A([\x01-\x09\x0B\x0C\x0E-\x7F]+)\z/ , 'list-select-opts' => qr/\A([\x20\x21\x23\x24\x26\x27\x2B-\x5B\x5E-\x7A\x7C-\x7E]*)\z/ , 'ignore-mailbox' => qr/\A([\x01-\x09\x0B\x0C\x0E-\x7F]+)\z/ ); }; -my ($DBFILE, $LOGGER_FD, %LIST); +my ($DBFILE, %LOGGER_CONF, %LIST); { $CONF->{_} //= {}; @@ -95,16 +96,15 @@ my ($DBFILE, $LOGGER_FD, %LIST); die "Missing option database" unless defined $DBFILE; $DBFILE = xdg_basedir( XDG_DATA_HOME => ".local/share", $NAME, $DBFILE ); + $LOGGER_CONF{'logger-prefix'} = $CONF->{_}->{'log-prefix'} // "%?n?%?m?%n(%m)&%n?: ?"; if (defined (my $l = $CONF->{_}->{logfile})) { require 'POSIX.pm'; require 'Time/HiRes.pm'; - open $LOGGER_FD, '>>', $l or die "Can't open $l: $!\n"; - $LOGGER_FD->autoflush(1); - my $flags = fcntl($LOGGER_FD, F_GETFD, 0) or die "fcntl F_GETFD: $!"; - fcntl($LOGGER_FD, F_SETFD, $flags | FD_CLOEXEC) or die "fcntl F_SETFD: $!"; - } - elsif ($CONFIG{debug}) { - $LOGGER_FD = \*STDERR; + open my $fd, '>>', $l or die "Can't open $l: $!\n"; + $fd->autoflush(1); + my $flags = fcntl($fd, F_GETFD, 0) or die "fcntl F_GETFD: $!"; + fcntl($fd, F_SETFD, $flags | FD_CLOEXEC) or die "fcntl F_SETFD: $!"; + $LOGGER_CONF{'logger-fd'} = $fd; } $LIST{mailbox} = [@ARGV]; @@ -149,8 +149,7 @@ my ($IMAP, $lIMAP, $rIMAP); sub cleanup() { undef $_ foreach grep defined, ($IMAP, $lIMAP, $rIMAP); logger(undef, "Cleaning up...") if $CONFIG{debug}; - $LOGGER_FD->close() if defined $LOGGER_FD and defined $LOGGER_FD->fileno - and $LOGGER_FD->fileno != fileno STDERR; + $LOGGER_CONF{'logger-fd'}->close() if defined $LOGGER_CONF{'logger-fd'}; $DBH->disconnect() if defined $DBH; } $SIG{INT} = sub { msg(undef, $!); cleanup(); exit 1; }; @@ -181,31 +180,25 @@ $SIG{TERM} = sub { cleanup(); exit 0; }; } sub msg($@) { + my %h = ( %LOGGER_CONF, name => shift ); + return Net::IMAP::InterIMAP::log(\%h, @_); +} +sub msg2($$@) { my $name = shift; - return unless @_; - logger($name, @_) if defined $LOGGER_FD and defined $LOGGER_FD->fileno - and $LOGGER_FD->fileno != fileno STDERR; - my $prefix = defined $name ? "$name: " : ''; - print STDERR $prefix, @_, "\n"; + my $mailbox = mbx_name($name => shift); + my %h = ( %LOGGER_CONF, name => $name, mailbox => $mailbox ); + return Net::IMAP::InterIMAP::log(\%h, @_); } sub logger($@) { - my $name = shift; - return unless @_ and defined $LOGGER_FD; - my $prefix = ''; - if (defined $LOGGER_FD and defined $LOGGER_FD->fileno - and $LOGGER_FD->fileno != fileno STDERR) { - my ($s, $us) = Time::HiRes::gettimeofday(); - $prefix = POSIX::strftime("%b %e %H:%M:%S", localtime($s)).".$us "; - } - $prefix .= "$name: " if defined $name; - $LOGGER_FD->say($prefix, @_); + my %h = ( %LOGGER_CONF, name => shift ); + return Net::IMAP::InterIMAP::logger(\%h, @_); } sub fail($@) { my $name = shift; msg($name, "ERROR: ", @_); exit 1; } -logger(undef, ">>> $NAME $VERSION"); +logger(undef, ">>> $NAME $VERSION") if $CONFIG{debug}; ############################################################################# @@ -216,7 +209,7 @@ foreach my $name (qw/local remote/) { $config{$_} = $CONFIG{$_} foreach grep {defined $CONFIG{$_}} qw/quiet debug/; $config{enable} = 'QRESYNC'; $config{name} = $name; - $config{'logger-fd'} = $LOGGER_FD if defined $LOGGER_FD; + $config{$_} = $LOGGER_CONF{$_} foreach keys %LOGGER_CONF; $config{'compress'} //= ($name eq 'local' ? 0 : 1); $config{keepalive} = 1 if $CONFIG{watch} and $config{type} ne 'tunnel'; @@ -447,7 +440,7 @@ sub db_create_mailbox($$) { $sth->bind_param(1, $mailbox, SQL_BLOB); $sth->bind_param(2, $subscribed, SQL_BOOLEAN); my $r = $sth->execute(); - msg("database", fmt("Created mailbox %d", $mailbox)); + msg("database", "Created mailbox ", mbx_pretty($mailbox)); return $r; } @@ -474,6 +467,7 @@ sub mbx_name($$) { } return defined $name ? ($CONF->{$name}->{"list-reference"} . $mailbox) : $mailbox; } +sub mbx_pretty($) { return mbx_name(undef, $_[0]); } # Transform mailbox name from local/remote IMAP server to the internal representation # (with \0 as hierarchy delimiters and without reference prefix). Return undef if @@ -553,7 +547,7 @@ if (defined $COMMAND and $COMMAND eq 'delete') { $sth->execute(); } $DBH->commit(); - msg("database", fmt("Removed mailbox %d", $mailbox)); + msg("database", "Removed mailbox ", mbx_pretty($mailbox)); } } exit 0; @@ -579,11 +573,14 @@ elsif (defined $COMMAND and $COMMAND eq 'rename') { foreach my $name (qw/local remote/) { my $mbx = mbx_name($name, $to); next unless $CONFIG{target}->{$name} and mbx_exists($name, $mbx); - fail($name, fmt("Mailbox %s exists. Run `$NAME --target=$name --delete %d` to delete.", $mbx, $to)); + fail($name, "Mailbox $mbx exists. Run `$NAME --target=$name --delete ", + mbx_pretty($to), "` to delete."); } # ensure the target name doesn't already exist in the database - fail("database", fmt("Mailbox %d exists. Run `$NAME --target=database --delete %d` to delete.", $to, $to)) + my $to_pretty = mbx_pretty($to); + fail("database", "Mailbox $to_pretty exists. Run `$NAME --target=database ", + "--delete $to_pretty` to delete.") if $CONFIG{target}->{database} and defined db_get_mailbox_idx($to); @@ -624,7 +621,8 @@ elsif (defined $COMMAND and $COMMAND eq 'rename') { $r += $sth_rename_children->execute(); $DBH->commit(); - msg("database", fmt("Renamed mailbox %d to %d", $from, $to)) if $r > 0; + msg("database", "Renamed mailbox ", mbx_pretty($from), " to ", + mbx_pretty($to)) if $r > 0; } exit 0; } @@ -703,7 +701,8 @@ sub sync_mailbox_list() { } elsif ($lExists or $rExists) { # $mailbox is on one server only - fail("database", fmt("Mailbox %d exists. Run `$NAME --target=database --delete %d` to delete.", $mailbox, $mailbox)) + my $str = mbx_pretty($mailbox); + fail("database", "Mailbox $str exists. Run `$NAME --target=database --delete $str` to delete.") if defined $idx; my ($name1, $name2, $mbx1, $mbx2) = $lExists ? ("local", "remote", $lMailbox, $rMailbox) : ("remote", "local", $rMailbox, $lMailbox); @@ -733,8 +732,7 @@ sub download_missing($$$@) { my @set = @_; my @uids; - my ($target, $f) = $source eq 'local' ? ('remote', '%l') : ('local', '%r'); - my $prefix = fmt("%s($f)", $source, $mailbox) unless $CONFIG{quiet}; + my $target = $source eq 'local' ? 'remote' : 'local'; my ($buff, $bufflen) = ([], 0); undef $buff if ($target eq 'local' ? $lIMAP : $rIMAP)->incapable('MULTIAPPEND'); @@ -747,7 +745,7 @@ sub download_missing($$$@) { my $from = first { defined $_ and @$_ } @{$mail->{ENVELOPE}}[2,3,4]; $from = (defined $from and defined $from->[0]->[2] and defined $from->[0]->[3]) ? $from->[0]->[2].'@'.$from->[0]->[3] : ''; - msg($prefix, "UID $mail->{UID} from <$from> ($mail->{INTERNALDATE})"); + msg2($source => $mailbox, "UID $mail->{UID} from <$from> ($mail->{INTERNALDATE})"); } callback_new_message($idx, $mailbox, $source, $mail, \@uids, $buff, \$bufflen) }); @@ -762,9 +760,9 @@ sub flag_conflict($$$$$) { my %flags = map {$_ => 1} (split(/ /, $lFlags), split(/ /, $rFlags)); my $flags = join ' ', sort(keys %flags); - msg(undef, fmt("WARNING: Conflicting flag update in %d for local UID $lUID (%s) ". - "and remote UID $rUID (%s). Setting both to the union (%s).", - $mailbox, $lFlags, $rFlags, $flags)); + msg(undef, "WARNING: Conflicting flag update in ", mbx_pretty($mailbox), + " for local UID $lUID ($lFlags) and remote UID $rUID ($rFlags).", + " Setting both to the union ($flags)."); return $flags } @@ -914,7 +912,8 @@ sub repair($) { } else { # conflict - msg(undef, fmt("WARNING: Missed flag update in %d for (lUID,rUID) = ($lUID,$rUID). Repairing.", $mailbox)) + msg(undef, "WARNING: Missed flag update in ", mbx_pretty($mailbox), + " for (lUID,rUID) = ($lUID,$rUID). Repairing.") if $lModSeq <= $cache->{lHIGHESTMODSEQ} and $rModSeq <= $cache->{rHIGHESTMODSEQ}; # set both $lUID and $rUID to the union of $lFlags and $rFlags my $flags = flag_conflict($mailbox, $lUID => $lFlags, $rUID => $rFlags); @@ -926,7 +925,8 @@ sub repair($) { } elsif (!defined $lModified->{$lUID} and !defined $rModified->{$rUID}) { push @delete_mapping, $lUID; - msg(undef, fmt("WARNING: Pair (lUID,rUID) = ($lUID,$rUID) vanished from %d. Repairing.", $mailbox)) + msg(undef, "WARNING: Pair (lUID,rUID) = ($lUID,$rUID) vanished from ", + mbx_pretty($mailbox), ". Repairing.") unless $lVanished{$lUID} and $rVanished{$rUID}; } elsif (!defined $lModified->{$lUID}) { @@ -934,7 +934,7 @@ sub repair($) { if ($lVanished{$lUID}) { push @rToRemove, $rUID; } else { - msg(fmt("local(%l)", $mailbox), "WARNING: UID $lUID disappeared. Downloading remote UID $rUID again."); + msg2(local => $mailbox, "WARNING: UID $lUID disappeared. Redownloading remote UID $rUID."); push @rMissing, $rUID; } } @@ -943,7 +943,7 @@ sub repair($) { if ($rVanished{$rUID}) { push @lToRemove, $lUID; } else { - msg(fmt("remote(%r)",$mailbox), "WARNING: UID $rUID disappeared. Downloading local UID $lUID again."); + msg2(remote => $mailbox, "WARNING: UID $rUID disappeared. Redownloading local UID $lUID."); push @lMissing, $lUID; } } @@ -973,17 +973,17 @@ sub repair($) { # Process UID found in IMAP but not in the mapping table. my @lDunno = keys %lVanished; my @rDunno = keys %rVanished; - msg(fmt("remote(%r)",$mailbox), "WARNING: No match for ".($#lDunno+1)." vanished local UID(s) " + msg2(remote => $mailbox, "WARNING: No match for ".($#lDunno+1)." vanished local UID(s) " .compact_set(@lDunno).". Ignoring.") if @lDunno; - msg(fmt("local(%l)",$mailbox), "WARNING: No match for ".($#rDunno+1)." vanished remote UID(s) " + msg2(local => $mailbox, "WARNING: No match for ".($#rDunno+1)." vanished remote UID(s) " .compact_set(@rDunno).". Ignoring.") if @rDunno; foreach my $lUID (keys %$lModified) { - msg(fmt("remote(%r)",$mailbox), "WARNING: No match for modified local UID $lUID. Downloading again."); + msg2(remote => $mailbox, "WARNING: No match for modified local UID $lUID. Redownloading."); push @lMissing, $lUID; } foreach my $rUID (keys %$rModified) { - msg(fmt("local(%l)",$mailbox), "WARNING: No match for modified remote UID $rUID. Downloading again."); + msg2(local => $mailbox, "WARNING: No match for modified remote UID $rUID. Redownloading."); push @rMissing, $rUID; } @@ -1063,9 +1063,9 @@ sub sync_known_messages($$) { } } - msg(fmt("remote(%r)",$mailbox), "WARNING: No match for ".($#lDunno+1)." vanished local UID(s) " + msg2(remote => $mailbox, "WARNING: No match for ".($#lDunno+1)." vanished local UID(s) " .compact_set(@lDunno).". Ignoring.") if @lDunno; - msg(fmt("local(%l)",$mailbox), "WARNING: No match for ".($#rDunno+1)." vanished remote UID(s) " + msg2(local => $mailbox, "WARNING: No match for ".($#rDunno+1)." vanished remote UID(s) " .compact_set(@rDunno).". Ignoring.") if @rDunno; $lIMAP->remove_message(@lToRemove) if @lToRemove; @@ -1098,7 +1098,7 @@ sub sync_known_messages($$) { my ($rUID) = $sth_get_remote_uid->fetchrow_array(); die if defined $sth_get_remote_uid->fetch(); # safety check if (!defined $rUID) { - msg(fmt("remote(%r)",$mailbox), "WARNING: No match for modified local UID $lUID. Try '--repair'."); + msg2(remote => $mailbox, "WARNING: No match for modified local UID $lUID. Try '--repair'."); } elsif (defined (my $rFlags = $rModified->{$rUID})) { unless ($lFlags eq $rFlags) { my $flags = flag_conflict($mailbox, $lUID => $lFlags, $rUID => $rFlags); @@ -1119,7 +1119,7 @@ sub sync_known_messages($$) { my ($lUID) = $sth_get_local_uid->fetchrow_array(); die if defined $sth_get_local_uid->fetch(); # safety check if (!defined $lUID) { - msg(fmt("local(%l)",$mailbox), "WARNING: No match for modified remote UID $rUID. Try '--repair'."); + msg2(local => $mailbox, "WARNING: No match for modified remote UID $rUID. Try '--repair'."); } elsif (!exists $lModified->{$lUID}) { # conflicts are taken care of above $lToUpdate{$rFlags} //= []; @@ -1151,8 +1151,7 @@ sub callback_new_message($$$$;$$$) { my $length = length ${$mail->{RFC822}}; if ($length == 0) { - my $prefix = $name eq "local" ? "local(%l)" : "remote(%r)"; - msg(fmt($prefix, $mailbox), "WARNING: Ignoring new 0-length message (UID $mail->{UID})"); + msg2($name => $mailbox, "WARNING: Ignoring new 0-length message (UID $mail->{UID})"); return; } @@ -1192,7 +1191,7 @@ sub callback_new_message_flush($$$@) { }); my ($lUIDs, $rUIDs) = $name eq 'local' ? (\@sUID,\@tUID) : (\@tUID,\@sUID); for (my $k=0; $k<=$#messages; $k++) { - logger(undef, fmt("Adding mapping (lUID,rUID) = ($lUIDs->[$k],$rUIDs->[$k]) for %d", $mailbox)) + logger(undef, "Adding mapping (lUID,rUID) = ($lUIDs->[$k],$rUIDs->[$k]) for ", mbx_pretty($mailbox)) if $CONFIG{debug}; $sth->bind_param(1, $idx, SQL_INTEGER); $sth->bind_param(2, $lUIDs->[$k], SQL_INTEGER); @@ -1322,7 +1321,7 @@ sub db_get_cache_by_idx($) { next unless grep { $_ eq $row->[1] } @MAILBOXES; # skip ignored mailboxes ($IDX, $MAILBOX) = @$row; - msg(undef, fmt("Resuming interrupted sync for %d", $MAILBOX)); + msg(undef, "Resuming interrupted sync for ", mbx_pretty($MAILBOX)); my $cache = db_get_cache_by_idx($IDX) // die; # safety check my ($lMailbox, $rMailbox) = map {mbx_name($_, $MAILBOX)} qw/local remote/; diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index 2d1f644..bb27009 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -17,6 +17,7 @@ #---------------------------------------------------------------------- package Net::IMAP::InterIMAP v0.0.5; +use v5.10.0; use warnings; use strict; @@ -280,7 +281,8 @@ our $IMAP_text; # # - 'name': An optional instance name to include in log messages. # -# - 'logger-fd': An optional filehandle to use for debug output. +# - 'logger-fd': An optional filehandle to use for debug output +# (default: STDERR). # # - 'keepalive': Whether to enable sending of keep-alive messages. # (type=imap or type=imaps). @@ -289,6 +291,7 @@ sub new($%) { my $class = shift; my $self = { @_ }; bless $self, $class; + require 'Time/HiRes.pm' if defined $self->{'logger-fd'}; # the IMAP state: one of 'UNAUTH', 'AUTH', 'SELECTED' or 'LOGOUT' # (cf RFC 3501 section 3) @@ -378,11 +381,6 @@ sub new($%) { # are considered. $self->{_MODIFIED} = {}; - if (defined $self->{'logger-fd'} and defined $self->{'logger-fd'}->fileno - and $self->{'logger-fd'}->fileno != fileno STDERR) { - require 'Time/HiRes.pm'; - } - # wait for the greeting my $x = $self->_getline(); $x =~ s/\A\* (OK|PREAUTH) // or $self->panic($x); @@ -539,32 +537,55 @@ sub DESTROY($) { # $self->log($message, [...]) # $self->logger($message, [...]) -# Log a $message. The latter method is used to log in the 'logger-fd', and -# add timestamps. +# Log a $message. The latter method is used to log in the 'logger-fd' +# (and adds timestamps). sub log($@) { my $self = shift; return unless @_; - $self->logger(@_) if defined $self->{'logger-fd'} and defined $self->{'logger-fd'}->fileno - and $self->{'logger-fd'}->fileno != fileno STDERR; - my $prefix = $self->{name} // ''; - $prefix .= "($self->{_SELECTED})" if $self->{_STATE} eq 'SELECTED'; - $prefix .= ': ' unless $prefix eq ''; - print STDERR $prefix, @_, "\n"; + my $prefix = _logger_prefix($self); + if (defined (my $fd = $self->{'logger-fd'})) { + say $fd _date(), " ", $prefix, @_; + } + say STDERR $prefix, @_; } sub logger($@) { my $self = shift; - return unless @_ and defined $self->{'logger-fd'}; - my $prefix = ''; - if (defined $self->{'logger-fd'}->fileno and defined $self->{'logger-fd'}->fileno - and $self->{'logger-fd'}->fileno != fileno STDERR) { - my ($s, $us) = Time::HiRes::gettimeofday(); - $prefix = POSIX::strftime("%b %e %H:%M:%S", localtime($s)).".$us"; - $prefix .= ' ' if defined $self->{name} or $self->{_STATE} eq 'SELECTED'; - } - $prefix .= $self->{name} if defined $self->{name}; - $prefix .= "($self->{_SELECTED})" if $self->{_STATE} eq 'SELECTED'; - $prefix .= ': ' unless $prefix eq ''; - $self->{'logger-fd'}->say($prefix, @_); + return unless @_; + my $prefix = _logger_prefix($self); + if (defined (my $fd = $self->{'logger-fd'})) { + say $fd _date(), " ", $prefix, @_; + } else { + say STDERR $prefix, @_; + } +} +sub _date() { + my ($s, $us) = Time::HiRes::gettimeofday(); + my $t = POSIX::strftime("%b %e %H:%M:%S", localtime($s)); + return "$t.$us"; # millisecond precision +} + +# $self->_logger_prefix() +# Format a prefix for logging with printf(3)-like sequences: +# %n: the object name +# %m: mailbox, either explicit named or selected +sub _logger_prefix($) { + my $self = shift; + my $format = $self->{'logger-prefix'} // return ""; + + my %seq = ( "%" => "%", m => $self->{mailbox}, n => $self->{name} ); + $seq{m} //= $self->{_SELECTED} // die + if defined $self->{_STATE} and $self->{_STATE} eq 'SELECTED'; + + do {} while + # rewrite conditionals (loop because of nesting) + $format =~ s#%\? ([[:alpha:]]) \? + ( (?: (?> (?: [^%&?\\] | %[^?] | \\[&?\\] )+ ) | (?R) )* ) + (?: \& ( (?: (?> (?: [^%&?\\] | %[^?] | \\[&?\\] )+ ) | (?R) )*) )? + \?# ($seq{$1} // "") ne "" ? $2 : ($3 // "") #agex; + + $format =~ s#\\([&?\\])#$1#g; # unescape remaining '&', '?' and '\' + $format =~ s#%([%mn])# $seq{$1} #ge; + return $format; } diff --git a/pullimap b/pullimap index 81811e9..dcbe59b 100755 --- a/pullimap +++ b/pullimap @@ -233,7 +233,8 @@ sub smtp_send(@) { my $IMAP = do { my %config = (%$CONF, %CONFIG{qw/quiet debug/}, name => $ARGV[0]); $config{keepalive} = 1 if defined $CONFIG{idle}; - $config{'logger-fd'} = \*STDERR if $CONFIG{debug}; + $config{'logger-prefix'} = "%?n?%?m?%n(%m)&%n?: ?"; + delete $config{mailbox}; # use SELECTed mailbox in log messages Net::IMAP::InterIMAP::->new( %config ); }; diff --git a/tests/01-rename-exists-db/run b/tests/01-rename-exists-db/run index 29cb075..aad7c44 100644 --- a/tests/01-rename-exists-db/run +++ b/tests/01-rename-exists-db/run @@ -9,6 +9,6 @@ doveadm -u "local" mailbox delete "t.o" doveadm -u "remote" mailbox delete "t\\o" ! interimap --rename "root.from" "t.o" -xgrep -Fx 'database: ERROR: Mailbox t.o exists. Run `interimap --target=database --delete t.o` to delete.' <"$STDERR" +xgrep -Fx 'database: ERROR: Mailbox t.o exists. Run `interimap --target=database --delete t.o` to delete.' <"$STDERR" # vim: set filetype=sh : diff --git a/tests/01-rename-exists-local/run b/tests/01-rename-exists-local/run index 17d8fcc..d82a0a4 100644 --- a/tests/01-rename-exists-local/run +++ b/tests/01-rename-exists-local/run @@ -8,6 +8,6 @@ check_mailbox_list doveadm -u "remote" mailbox delete "t\\o" ! interimap --rename "root.from" "t.o" -xgrep -Fx 'local: ERROR: Mailbox t.o exists. Run `interimap --target=local --delete t.o` to delete.' <"$STDERR" +xgrep -Fx 'local: ERROR: Mailbox t.o exists. Run `interimap --target=local --delete t.o` to delete.' <"$STDERR" # vim: set filetype=sh : diff --git a/tests/01-rename-exists-remote/run b/tests/01-rename-exists-remote/run index c867a77..28af1fc 100644 --- a/tests/01-rename-exists-remote/run +++ b/tests/01-rename-exists-remote/run @@ -8,6 +8,6 @@ check_mailbox_list doveadm -u "local" mailbox delete "t.o" ! interimap --rename "root.from" "t.o" -xgrep -Fx 'remote: ERROR: Mailbox t\o exists. Run `interimap --target=remote --delete t.o` to delete.' <"$STDERR" +xgrep -Fx 'remote: ERROR: Mailbox t\o exists. Run `interimap --target=remote --delete t.o` to delete.' <"$STDERR" # vim: set filetype=sh : diff --git a/tests/03-sync-mailbox-list/run b/tests/03-sync-mailbox-list/run index e9fda06..b506204 100644 --- a/tests/03-sync-mailbox-list/run +++ b/tests/03-sync-mailbox-list/run @@ -24,7 +24,7 @@ check_mailbox_list -s # delete a mailbox one server and verify that synchronization fails as it's still in the database doveadm -u "remote" mailbox delete "foo~baz" ! interimap -xgrep -Fx 'database: ERROR: Mailbox foo.baz exists. Run `interimap --target=database --delete foo.baz` to delete.' <"$STDERR" +xgrep -Fx 'database: ERROR: Mailbox foo.baz exists. Run `interimap --target=database --delete foo.baz` to delete.' <"$STDERR" interimap --target="database" --delete "foo.baz" xgrep -Fx 'database: Removed mailbox foo.baz' <"$STDERR" interimap # create again @@ -33,7 +33,7 @@ xgrep -Fx 'remote: Created mailbox foo~baz' <"$STDERR" doveadm -u "local" mailbox delete "foo.bar" ! interimap -xgrep -Fx 'database: ERROR: Mailbox foo.bar exists. Run `interimap --target=database --delete foo.bar` to delete.' <"$STDERR" +xgrep -Fx 'database: ERROR: Mailbox foo.bar exists. Run `interimap --target=database --delete foo.bar` to delete.' <"$STDERR" interimap --target="database" --delete "foo.bar" xgrep -Fx 'database: Removed mailbox foo.bar' <"$STDERR" interimap diff --git a/tests/05-repair/run b/tests/05-repair/run index 15553e0..66f9ce9 100644 --- a/tests/05-repair/run +++ b/tests/05-repair/run @@ -77,10 +77,10 @@ xcgrep 5 '^WARNING: Conflicting flag update in foo\.bar ' <"$STDERR" xcgrep 1 -E '^WARNING: Pair \(lUID,rUID\) = \([0-9]+,[0-9]+\) vanished from foo\.bar\. Repairing\.$' <"$STDERR" # 6-1 (luid 2 <-> ruid 10 is gone from both) -xcgrep 5 -E '^local\(foo\.bar\): WARNING: UID [0-9]+ disappeared\. Downloading remote UID [0-9]+ again\.$' <"$STDERR" +xcgrep 5 -E '^local\(foo\.bar\): WARNING: UID [0-9]+ disappeared. Redownloading remote UID [0-9]+\.$' <"$STDERR" # 6-1 (luid 2 <-> ruid 10 is gone from both) -xcgrep 3 -E '^remote\(foo~bar\): WARNING: UID [0-9]+ disappeared\. Downloading local UID [0-9]+ again\.$' <"$STDERR" +xcgrep 3 -E '^remote\(foo~bar\): WARNING: UID [0-9]+ disappeared. Redownloading local UID [0-9]+\.$' <"$STDERR" xgrep -E '^local\(baz\): Removed 24 UID\(s\) ' <"$STDERR" xgrep -E '^remote\(baz\): Removed 5 UID\(s\) ' <"$STDERR" @@ -92,7 +92,7 @@ xgrep -E '^remote\(foo~bar\): Updated flags \(\\Answered \\Seen\) for UID 8$' < xgrep -E '^remote\(foo~bar\): Updated flags \(\\Answered\) for UID 3,12,16$' <"$STDERR" # luid 17 -xcgrep 1 -E '^remote\(foo~bar\): WARNING: No match for modified local UID [0-9]+\. Downloading again\.' <"$STDERR" +xcgrep 1 -E '^remote\(foo~bar\): WARNING: No match for modified local UID [0-9]+. Redownloading\.' <"$STDERR" xgrep -E '^local\(foo\.bar\): Added 5 UID\(s\) ' <"$STDERR" xgrep -E '^remote\(foo~bar\): Added 4 UID\(s\) ' <"$STDERR" -- cgit v1.2.3