mirror of
https://github.com/vale981/kindle_fetch
synced 2025-03-04 08:31:38 -05:00
provide exectuable for flake and add readme
This commit is contained in:
parent
a759dfb98d
commit
62408b2126
4 changed files with 89 additions and 20 deletions
35
README.md
Normal file
35
README.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Kindle (Scribe) Email Fetch Hack
|
||||||
|
|
||||||
|
This is a quick-and-dirty python script to log into an IMAP server,
|
||||||
|
monitor incoming messages for the ones that contain the links to the
|
||||||
|
PDFs that you sent from the Kindle scribe. Once such an email is found
|
||||||
|
the pdf linked therein is downloaded to a local directory and the
|
||||||
|
email is deleted. The latest downloaded file is also copied to a
|
||||||
|
preset filename to make it easier to find it. I'm always running
|
||||||
|
`zathura ~/kindle_dump/latest.pdf` to have the latest kindle pdf
|
||||||
|
visible.
|
||||||
|
|
||||||
|
## Installation / Usage
|
||||||
|
|
||||||
|
Either clone this repo and use `poerty install` and the like or run the nix flake with `nix run github:vale981/kindle_fetch -- [args]`.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: kindle_fetch [-h] [--outdir OUTDIR] [--current_file CURRENT_FILE] [--imap_folder IMAP_FOLDER]
|
||||||
|
server user pass_command
|
||||||
|
|
||||||
|
Monitors you Email and automatically downloads the notes sent to it.
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
server the IMAP server to connect to
|
||||||
|
user the IMAP username
|
||||||
|
pass_command a shell command that returns the password to the server
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--outdir OUTDIR the directory to dump the note PDFs in
|
||||||
|
--current_file CURRENT_FILE
|
||||||
|
the path to the file that will contain the the most currently downloaded pdf relative to
|
||||||
|
`outdir`
|
||||||
|
--imap_folder IMAP_FOLDER
|
||||||
|
the folder to monitor for new messages
|
||||||
|
```
|
15
flake.nix
15
flake.nix
|
@ -15,16 +15,23 @@
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
poetry2nix = inputs.poetry2nix.lib.mkPoetry2Nix { inherit pkgs; };
|
poetry2nix = inputs.poetry2nix.lib.mkPoetry2Nix { inherit pkgs; };
|
||||||
|
kindleFetch = poetry2nix.mkPoetryApplication {
|
||||||
|
projectDir = self;
|
||||||
|
preferWheels = true;
|
||||||
|
};
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages = {
|
packages = {
|
||||||
kindleFetch = poetry2nix.mkPoetryApplication {
|
kindleFetch = kindleFetch;
|
||||||
projectDir = self;
|
|
||||||
preferWheels = true;
|
|
||||||
};
|
|
||||||
default = self.packages.${system}.kindleFetch;
|
default = self.packages.${system}.kindleFetch;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
apps.default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${kindleFetch}/bin/kindle_fetch";
|
||||||
|
};
|
||||||
|
|
||||||
# Shell for app dependencies.
|
# Shell for app dependencies.
|
||||||
#
|
#
|
||||||
# nix develop
|
# nix develop
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
#! /usr/bin/env python
|
#! /usr/bin/env python
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
import shutil
|
import shutil
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@ -12,12 +9,9 @@ import subprocess
|
||||||
from aioimaplib import aioimaplib
|
from aioimaplib import aioimaplib
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
import re
|
import re
|
||||||
from asyncio import run, wait_for
|
from asyncio import wait_for
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from email.message import Message
|
|
||||||
from email.parser import BytesHeaderParser, BytesParser
|
from email.parser import BytesHeaderParser, BytesParser
|
||||||
from typing import Collection
|
|
||||||
from contextlib import suppress
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,6 +40,7 @@ class Options:
|
||||||
|
|
||||||
|
|
||||||
def get_document_title(header_string):
|
def get_document_title(header_string):
|
||||||
|
"""Get the title of the document from the email header."""
|
||||||
m = re.search(r'"(.*?)" from your Kindle', header_string)
|
m = re.search(r'"(.*?)" from your Kindle', header_string)
|
||||||
|
|
||||||
if not m:
|
if not m:
|
||||||
|
@ -55,6 +50,10 @@ def get_document_title(header_string):
|
||||||
|
|
||||||
|
|
||||||
def get_download_link(text):
|
def get_download_link(text):
|
||||||
|
"""
|
||||||
|
Get the download link and whether the file is the full document or
|
||||||
|
just `page` pages from the email body.
|
||||||
|
"""
|
||||||
m = re.search(r"\[Download PDF\]\((.*?)\)", text)
|
m = re.search(r"\[Download PDF\]\((.*?)\)", text)
|
||||||
|
|
||||||
if not m:
|
if not m:
|
||||||
|
@ -75,6 +74,12 @@ MessageAttributes = namedtuple("MessageAttributes", "uid flags sequence_number")
|
||||||
|
|
||||||
|
|
||||||
async def fetch_messages_headers(imap_client: aioimaplib.IMAP4_SSL, max_uid: int):
|
async def fetch_messages_headers(imap_client: aioimaplib.IMAP4_SSL, max_uid: int):
|
||||||
|
"""
|
||||||
|
Fetch the headers of the messages in the mailbox.
|
||||||
|
|
||||||
|
Pretty much stolen from the `aioimaplib` examples.
|
||||||
|
"""
|
||||||
|
|
||||||
response = await imap_client.uid(
|
response = await imap_client.uid(
|
||||||
"fetch",
|
"fetch",
|
||||||
"%d:*" % (max_uid + 1),
|
"%d:*" % (max_uid + 1),
|
||||||
|
@ -112,22 +117,31 @@ async def fetch_messages_headers(imap_client: aioimaplib.IMAP4_SSL, max_uid: int
|
||||||
|
|
||||||
|
|
||||||
async def fetch_message_body(imap_client: aioimaplib.IMAP4_SSL, uid: int):
|
async def fetch_message_body(imap_client: aioimaplib.IMAP4_SSL, uid: int):
|
||||||
|
"""Fetch the message body of the message with the given ``uid``."""
|
||||||
dwnld_resp = await imap_client.uid("fetch", str(uid), "BODY.PEEK[]")
|
dwnld_resp = await imap_client.uid("fetch", str(uid), "BODY.PEEK[]")
|
||||||
return BytesParser().parsebytes(dwnld_resp.lines[1])
|
return BytesParser().parsebytes(dwnld_resp.lines[1])
|
||||||
|
|
||||||
|
|
||||||
async def remove_message(imap_client: aioimaplib.IMAP4_SSL, uid: int):
|
async def remove_message(imap_client: aioimaplib.IMAP4_SSL, uid: int):
|
||||||
|
"""Mark the message with the given ``uid`` as deleted and expunge it."""
|
||||||
await imap_client.uid("store", str(uid), "+FLAGS (\Deleted \Seen)")
|
await imap_client.uid("store", str(uid), "+FLAGS (\Deleted \Seen)")
|
||||||
return await imap_client.expunge()
|
return await imap_client.expunge()
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_new_message(imap_client, options: Options):
|
async def wait_for_new_message(imap_client, options: Options):
|
||||||
|
"""
|
||||||
|
Wait for a new message to arrive in the mailbox, detect Kindle
|
||||||
|
messages and download the PDF linked in if possible.
|
||||||
|
"""
|
||||||
|
|
||||||
persistent_max_uid = 1
|
persistent_max_uid = 1
|
||||||
persistent_max_uid, head = await fetch_messages_headers(
|
persistent_max_uid, head = await fetch_messages_headers(
|
||||||
imap_client, persistent_max_uid
|
imap_client, persistent_max_uid
|
||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
print("Waiting for new message")
|
print("Waiting for new message")
|
||||||
|
|
||||||
idle_task = await imap_client.idle_start(timeout=60)
|
idle_task = await imap_client.idle_start(timeout=60)
|
||||||
msg = await imap_client.wait_server_push()
|
msg = await imap_client.wait_server_push()
|
||||||
imap_client.idle_done()
|
imap_client.idle_done()
|
||||||
|
@ -169,11 +183,16 @@ async def wait_for_new_message(imap_client, options: Options):
|
||||||
|
|
||||||
await remove_message(imap_client, persistent_max_uid)
|
await remove_message(imap_client, persistent_max_uid)
|
||||||
|
|
||||||
# await asyncio.wait_for(idle_task, timeout=5)
|
|
||||||
# print("ending idle")
|
|
||||||
|
|
||||||
|
|
||||||
async def make_client(host, user, password, folder):
|
async def make_client(host, user, password, folder):
|
||||||
|
"""Connect to the IMAP server and login.
|
||||||
|
|
||||||
|
:param host: the IMAP server to connect to
|
||||||
|
:param user: the IMAP username
|
||||||
|
:param password: the password to the server
|
||||||
|
:param folder: the folder to monitor for new messages
|
||||||
|
"""
|
||||||
|
|
||||||
imap_client = aioimaplib.IMAP4_SSL(host=host)
|
imap_client = aioimaplib.IMAP4_SSL(host=host)
|
||||||
await imap_client.wait_hello_from_server()
|
await imap_client.wait_hello_from_server()
|
||||||
await imap_client.login(user, password)
|
await imap_client.login(user, password)
|
||||||
|
@ -185,7 +204,7 @@ async def make_client(host, user, password, folder):
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="Kindle Fetcher",
|
prog="kindle_fetch",
|
||||||
description="Monitors you Email and automatically downloads the notes sent to it.",
|
description="Monitors you Email and automatically downloads the notes sent to it.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -232,13 +251,17 @@ def parse_args():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def main():
|
||||||
options = parse_args()
|
options = parse_args()
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
client = loop.run_until_complete(
|
try:
|
||||||
make_client(options.server, options.user, options.password, options.mailbox)
|
client = loop.run_until_complete(
|
||||||
)
|
make_client(options.server, options.user, options.password, options.mailbox)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to connect to the server: {e}")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
loop.run_until_complete(wait_for_new_message(client, options))
|
loop.run_until_complete(wait_for_new_message(client, options))
|
||||||
loop.run_until_complete(client.logout())
|
loop.run_until_complete(client.logout())
|
|
@ -1,5 +1,5 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "kindlefetch"
|
name = "kindle_fetch"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Valentin Boettcher <hiro@protagon.space>"]
|
authors = ["Valentin Boettcher <hiro@protagon.space>"]
|
||||||
|
@ -14,3 +14,7 @@ aioimaplib = "^1.0.1"
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
kindle_fetch = 'kindle_fetch.kindle_fetch:main'
|
||||||
|
|
Loading…
Add table
Reference in a new issue