Skip to content

Commit

Permalink
implement --pass-pinentry option
Browse files Browse the repository at this point in the history
  • Loading branch information
oxij committed Nov 13, 2023
1 parent 59c090a commit 2bc2135
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 15 deletions.
38 changes: 25 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,7 @@ DEFAULT="$HOME/Mail/backup"
EOF
# backup all your mail from GMail
read password
echo "$password" > ./password.txt
imaparms fetch --host imap.gmail.com --user [email protected] --passfile ./password.txt --mda maildrop --all-folders --all
rm ./password.txt
imaparms fetch --host imap.gmail.com --user [email protected] --pass-pinentry --mda maildrop --all-folders --all
```

For GMail you will have to create and use application-specific password, which requires enabling 2FA, [see below for more info](#gmail).
Expand All @@ -81,21 +78,21 @@ To make the above efficient you have to sacrifice either `SEEN` or `FLAGGED` IMA

```
# mark all messages as UNSEEN
imaparms mark --host imap.gmail.com --user [email protected] --passfile ./password.txt --all-folders unseen
imaparms mark --host imap.gmail.com --user [email protected] --pass-pinentry --all-folders unseen
# fetch UNSEEN and mark as SEEN as you go
# this can be interrrupted and restarted and it will continue from where it left off
imaparms mark --host imap.gmail.com --user [email protected] --passfile ./password.txt --all-folders --unseen
imaparms mark --host imap.gmail.com --user [email protected] --pass-pinentry --all-folders --unseen
```

or

```
# mark all messages as UNFLAGGED
imaparms mark --host imap.gmail.com --user [email protected] --passfile ./password.txt --all-folders unflagged
imaparms mark --host imap.gmail.com --user [email protected] --pass-pinentry --all-folders unflagged
# similarly
imaparms mark --host imap.gmail.com --user [email protected] --passfile ./password.txt --all-folders --unflagged
imaparms mark --host imap.gmail.com --user [email protected] --pass-pinentry --all-folders --unflagged
```

This, of course, means that if you open or "mark as read" a message in GMail's web-mail UI while using `imaparms --unseen`, or flag (star) it there while using `imaparms --unflagged`, `imaparms` will ignore the message on the next `fetch`.
Expand Down Expand Up @@ -300,7 +297,7 @@ Logins to a specified server, performs specified actions on all messages matchin
- `delete (expire)`
: delete matching messages from specified folders

### imaparms list [--debug] [--dry-run] [--plain | --ssl | --starttls] [--host HOST] [--port PORT] [--user USER] [--passfile PASSFILE | --passcmd PASSCMD] [--store-number INT] [--fetch-number INT] [--batch-number INT] [--batch-size INT] [--every SECONDS] [--every-add-random ADD]
### imaparms list [--debug] [--dry-run] [--plain | --ssl | --starttls] [--host HOST] [--port PORT] [--user USER] [--pass-pinentry | --passfile PASSFILE | --passcmd PASSCMD] [--store-number INT] [--fetch-number INT] [--batch-number INT] [--batch-size INT] [--every SECONDS] [--every-add-random ADD]

Login, perform IMAP `LIST` command to get all folders, print them one per line.

Expand All @@ -327,6 +324,8 @@ Login, perform IMAP `LIST` command to get all folders, print them one per line.

- `--user USER`
: username on the server (required)
- `--pass-pinentry`
: read the password via `pinentry`
- `--passfile PASSFILE, --pass-file PASSFILE`
: file containing the password on its first line
- `--passcmd PASSCMD, --pass-cmd PASSCMD`
Expand Down Expand Up @@ -354,7 +353,7 @@ Login, perform IMAP `LIST` command to get all folders, print them one per line.
if you set in large enough to cover the longest single-server `fetch`, it will prevent any of the servers learning anything about the data on other servers;
if you run `imaparms` on a machine that disconnects from the Internet when you go to sleep and you set it large enough, it will help in preventing the servers from collecting data about your sleep cycle

### imaparms count [--debug] [--dry-run] [--plain | --ssl | --starttls] [--host HOST] [--port PORT] [--user USER] [--passfile PASSFILE | --passcmd PASSCMD] [--store-number INT] [--fetch-number INT] [--batch-number INT] [--batch-size INT] [--every SECONDS] [--every-add-random ADD] [--all-folders | --folder NAME] [--not-folder NAME] [--all | [--seen | --unseen |] [--flagged | --unflagged]] [--older-than DAYS] [--newer-than DAYS] [--older-than-timestamp-in PATH] [--newer-than-timestamp-in PATH] [--older-than-mtime-of PATH] [--newer-than-mtime-of PATH] [--from ADDRESS] [--not-from ADDRESS] [--porcelain]
### imaparms count [--debug] [--dry-run] [--plain | --ssl | --starttls] [--host HOST] [--port PORT] [--user USER] [--pass-pinentry | --passfile PASSFILE | --passcmd PASSCMD] [--store-number INT] [--fetch-number INT] [--batch-number INT] [--batch-size INT] [--every SECONDS] [--every-add-random ADD] [--all-folders | --folder NAME] [--not-folder NAME] [--all | [--seen | --unseen |] [--flagged | --unflagged]] [--older-than DAYS] [--newer-than DAYS] [--older-than-timestamp-in PATH] [--newer-than-timestamp-in PATH] [--older-than-mtime-of PATH] [--newer-than-mtime-of PATH] [--from ADDRESS] [--not-from ADDRESS] [--porcelain]

Login, (optionally) perform IMAP `LIST` command to get all folders, perform IMAP `SEARCH` command with specified filters in each folder, print message counts for each folder one per line.

Expand Down Expand Up @@ -385,6 +384,8 @@ Login, (optionally) perform IMAP `LIST` command to get all folders, perform IMAP

- `--user USER`
: username on the server (required)
- `--pass-pinentry`
: read the password via `pinentry`
- `--passfile PASSFILE, --pass-file PASSFILE`
: file containing the password on its first line
- `--passcmd PASSCMD, --pass-cmd PASSCMD`
Expand Down Expand Up @@ -450,7 +451,7 @@ Login, (optionally) perform IMAP `LIST` command to get all folders, perform IMAP
- `--unflagged`
: operate on messages not marked as `FLAGGED`

### imaparms mark [--debug] [--dry-run] [--plain | --ssl | --starttls] [--host HOST] [--port PORT] [--user USER] [--passfile PASSFILE | --passcmd PASSCMD] [--store-number INT] [--fetch-number INT] [--batch-number INT] [--batch-size INT] [--every SECONDS] [--every-add-random ADD] (--all-folders | --folder NAME) [--not-folder NAME] [--all | [--seen | --unseen |] [--flagged | --unflagged]] [--older-than DAYS] [--newer-than DAYS] [--older-than-timestamp-in PATH] [--newer-than-timestamp-in PATH] [--older-than-mtime-of PATH] [--newer-than-mtime-of PATH] [--from ADDRESS] [--not-from ADDRESS] {seen,unseen,flagged,unflagged}
### imaparms mark [--debug] [--dry-run] [--plain | --ssl | --starttls] [--host HOST] [--port PORT] [--user USER] [--pass-pinentry | --passfile PASSFILE | --passcmd PASSCMD] [--store-number INT] [--fetch-number INT] [--batch-number INT] [--batch-size INT] [--every SECONDS] [--every-add-random ADD] (--all-folders | --folder NAME) [--not-folder NAME] [--all | [--seen | --unseen |] [--flagged | --unflagged]] [--older-than DAYS] [--newer-than DAYS] [--older-than-timestamp-in PATH] [--newer-than-timestamp-in PATH] [--older-than-mtime-of PATH] [--newer-than-mtime-of PATH] [--from ADDRESS] [--not-from ADDRESS] {seen,unseen,flagged,unflagged}

Login, perform IMAP `SEARCH` command with specified filters for each folder, mark resulting messages in specified way by issuing IMAP `STORE` commands.

Expand All @@ -477,6 +478,8 @@ Login, perform IMAP `SEARCH` command with specified filters for each folder, mar

- `--user USER`
: username on the server (required)
- `--pass-pinentry`
: read the password via `pinentry`
- `--passfile PASSFILE, --pass-file PASSFILE`
: file containing the password on its first line
- `--passcmd PASSCMD, --pass-cmd PASSCMD`
Expand Down Expand Up @@ -550,7 +553,7 @@ Login, perform IMAP `SEARCH` command with specified filters for each folder, mar
- `flag`: add `FLAGGED` flag, sets `--unflagged` if no message search filter is specified
- `unflag`: remove `FLAGGED` flag, sets `--flagged` if no message search filter is specified

### imaparms fetch [--debug] [--dry-run] [--plain | --ssl | --starttls] [--host HOST] [--port PORT] [--user USER] [--passfile PASSFILE | --passcmd PASSCMD] [--store-number INT] [--fetch-number INT] [--batch-number INT] [--batch-size INT] [--every SECONDS] [--every-add-random ADD] --mda COMMAND [--new-mail-cmd NEW_MAIL_CMD] [--all-folders | --folder NAME] [--not-folder NAME] [--all | [--seen | --unseen |] [--flagged | --unflagged]] [--older-than DAYS] [--newer-than DAYS] [--older-than-timestamp-in PATH] [--newer-than-timestamp-in PATH] [--older-than-mtime-of PATH] [--newer-than-mtime-of PATH] [--from ADDRESS] [--not-from ADDRESS] [--mark {auto,noop,seen,unseen,flagged,unflagged}]
### imaparms fetch [--debug] [--dry-run] [--plain | --ssl | --starttls] [--host HOST] [--port PORT] [--user USER] [--pass-pinentry | --passfile PASSFILE | --passcmd PASSCMD] [--store-number INT] [--fetch-number INT] [--batch-number INT] [--batch-size INT] [--every SECONDS] [--every-add-random ADD] --mda COMMAND [--new-mail-cmd NEW_MAIL_CMD] [--all-folders | --folder NAME] [--not-folder NAME] [--all | [--seen | --unseen |] [--flagged | --unflagged]] [--older-than DAYS] [--newer-than DAYS] [--older-than-timestamp-in PATH] [--newer-than-timestamp-in PATH] [--older-than-mtime-of PATH] [--newer-than-mtime-of PATH] [--from ADDRESS] [--not-from ADDRESS] [--mark {auto,noop,seen,unseen,flagged,unflagged}]

Login, perform IMAP `SEARCH` command with specified filters for each folder, fetch resulting messages in (configurable) batches, feed each batch of messages to an MDA, mark each message for which MDA succeded in a specified way by issuing IMAP `STORE` commands.

Expand All @@ -577,6 +580,8 @@ Login, perform IMAP `SEARCH` command with specified filters for each folder, fet

- `--user USER`
: username on the server (required)
- `--pass-pinentry`
: read the password via `pinentry`
- `--passfile PASSFILE, --pass-file PASSFILE`
: file containing the password on its first line
- `--passcmd PASSCMD, --pass-cmd PASSCMD`
Expand Down Expand Up @@ -660,7 +665,7 @@ Login, perform IMAP `SEARCH` command with specified filters for each folder, fet
- `flagged`: add `FLAGGED` flag
- `unflagged`: remove `FLAGGED` flag

### imaparms delete [--debug] [--dry-run] [--plain | --ssl | --starttls] [--host HOST] [--port PORT] [--user USER] [--passfile PASSFILE | --passcmd PASSCMD] [--store-number INT] [--fetch-number INT] [--batch-number INT] [--batch-size INT] [--every SECONDS] [--every-add-random ADD] (--all-folders | --folder NAME) [--not-folder NAME] [--all | [--seen | --unseen |] [--flagged | --unflagged]] [--older-than DAYS] [--newer-than DAYS] [--older-than-timestamp-in PATH] [--newer-than-timestamp-in PATH] [--older-than-mtime-of PATH] [--newer-than-mtime-of PATH] [--from ADDRESS] [--not-from ADDRESS] [--method {auto,delete,delete-noexpunge,gmail-trash}]
### imaparms delete [--debug] [--dry-run] [--plain | --ssl | --starttls] [--host HOST] [--port PORT] [--user USER] [--pass-pinentry | --passfile PASSFILE | --passcmd PASSCMD] [--store-number INT] [--fetch-number INT] [--batch-number INT] [--batch-size INT] [--every SECONDS] [--every-add-random ADD] (--all-folders | --folder NAME) [--not-folder NAME] [--all | [--seen | --unseen |] [--flagged | --unflagged]] [--older-than DAYS] [--newer-than DAYS] [--older-than-timestamp-in PATH] [--newer-than-timestamp-in PATH] [--older-than-mtime-of PATH] [--newer-than-mtime-of PATH] [--from ADDRESS] [--not-from ADDRESS] [--method {auto,delete,delete-noexpunge,gmail-trash}]

Login, perform IMAP `SEARCH` command with specified filters for each folder, delete them from the server using a specified method.

Expand Down Expand Up @@ -695,6 +700,8 @@ Login, perform IMAP `SEARCH` command with specified filters for each folder, del

- `--user USER`
: username on the server (required)
- `--pass-pinentry`
: read the password via `pinentry`
- `--passfile PASSFILE, --pass-file PASSFILE`
: file containing the password on its first line
- `--passcmd PASSCMD, --pass-cmd PASSCMD`
Expand Down Expand Up @@ -772,6 +779,11 @@ Specifying `--folder` multiple times will perform the specified action on all sp

- List all available IMAP folders and count how many messages they contain:

- with the password taken from `pinentry`:
```
imaparms count --ssl --host imap.example.com --user [email protected] --pass-pinentry
```
- with the password taken from the first line of the given file:
```
imaparms count --ssl --host imap.example.com --user [email protected] --passfile /path/to/file/containing/[email protected]
Expand Down
32 changes: 30 additions & 2 deletions imaparms/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ def handle_signals() -> None:
signal.signal(signal.SIGTERM, sig_handler)
signal.signal(signal.SIGUSR1, sig_unsleep)

def pinentry(host : str, user : str) -> str:
with subprocess.Popen(["pinentry"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
def check(beginning : str) -> str:
res = p.stdout.readline().decode(defenc) # type: ignore
if not res.endswith("\n") or not res.startswith(beginning):
raise Failure("pinentry conversation failed")
return res[len(beginning):-1]
check("OK ")
def opt(what : str, beginning : str) -> str:
p.stdin.write(what.encode(defenc) + b"\n") # type: ignore
p.stdin.flush() # type: ignore
return check(beginning)
opt("SETDESC " + gettext("Please enter the passphrase for user %s on host %s") % (user, host), "OK")
opt("SETPROMPT " + gettext("Passphrase:"), "OK")
pin = opt("GETPIN", "D ")
return pin

def imap_parse_data(data : bytes, literals : _t.List[bytes] = [], top_level : bool = True) -> _t.Tuple[_t.Any, bytes]:
"Parse IMAP response string into a tree of strings."
acc : _t.List[bytes] = []
Expand Down Expand Up @@ -766,6 +783,10 @@ def add_examples(fmt : _t.Any) -> None:

fmt.start_section(_("List all available IMAP folders and count how many messages they contain"))

fmt.start_section(_("with the password taken from `pinentry`"))
fmt.add_code(f'{__package__} count --ssl --host imap.example.com --user [email protected] --pass-pinentry')
fmt.end_section()

fmt.start_section(_("with the password taken from the first line of the given file"))
fmt.add_code(f'{__package__} count --ssl --host imap.example.com --user [email protected] --passfile /path/to/file/containing/[email protected]')
fmt.end_section()
Expand Down Expand Up @@ -921,7 +942,9 @@ def __call__(self, parser : _t.Any, cfg : _t.Any, value : _t.Any, option_string
user = cfg.user
cfg.user = None

if self.ptype == "file":
if self.ptype == "pinentry":
password = pinentry(host, user)
elif self.ptype == "file":
with open(value, "rb") as f:
password = f.readline().decode(defenc)
elif self.ptype == "cmd":
Expand Down Expand Up @@ -962,6 +985,7 @@ def add_common(cmd : _t.Any) -> None:
agrp.add_argument("--user", type=str, help=_("username on the server (required)"))

grp = agrp.add_mutually_exclusive_group()
grp.add_argument("--pass-pinentry", nargs=0, action=EmitAccount, default="pinentry", help=_("read the password via `pinentry`"))
grp.add_argument("--passfile", "--pass-file", action=EmitAccount, default="file", help=_("file containing the password on its first line"))
grp.add_argument("--passcmd", "--pass-cmd", action=EmitAccount, default="cmd", help=_("shell command that returns the password as the first line of its stdout"))
grp.set_defaults(password = None)
Expand Down Expand Up @@ -1114,7 +1138,11 @@ def no_cmd(args : _t.Any) -> None:
cmd.set_defaults(func=cmd_action)
cmd.set_defaults(command="delete")

args = parser.parse_args(sys.argv[1:])
try:
args = parser.parse_args(sys.argv[1:])
except CatastrophicFailure as exc:
error(exc.show())
sys.exit(1)

if args.help_markdown:
parser.set_formatter_class(argparse.MarkdownBetterHelpFormatter)
Expand Down

0 comments on commit 2bc2135

Please sign in to comment.