PDF-Übertragung zum reMarkable per E-Mail/GMX
(automatisiert, ähnlich "reMailable" / "Send to Kindle")
sudo)rm-xyz123@gmx.de – lang/kompliziert)reMarkable + reMarkable_FailedreMarkable, Rest → PapierkorbGMX_USER="deine-email@gmx.de" GMX_PASS="dein-app-passwort"
[Unit] Description=GMX to reMarkable ingest After=network-online.target Wants=network-online.target [Service] Type=oneshot User=rm_ingest EnvironmentFile=/etc/rm-ingest/gmx.env ExecStart=/usr/bin/python3 -u /opt/rm-ingest/gmx_to_rm.py [Install] WantedBy=multi-user.target
[Unit] Description=GMX to reMarkable every 5min (07-23) [Timer] OnCalendar=*-*-* 07..22:55/5:00 Persistent=true [Install] WantedBy=timers.target
Kopiere komplett in /opt/rm-ingest/gmx_to_rm.py:
#!/usr/bin/env python3
import imaplib
import email
from email import policy
from email.header import decode_header
import os
import re
import subprocess
import tempfile
# Konfiguration
IMAP_HOST, IMAP_PORT = "imap.gmx.com", 993
MAILBOX_IN, MAILBOX_TRASH = "reMarkable", "Papierkorb"
RMAPI_BIN, RM_TARGET = "rmapi", "/"
GMX_USER, GMX_PASS = os.environ["GMX_USER"], os.environ["GMX_PASS"]
def log(msg: str) -> None:
print(msg, flush=True)
def decode_mime_words(s: str | None) -> str | None:
if not s:
return s
parts = decode_header(s)
out = []
for val, enc in parts:
if isinstance(val, bytes):
out.append(val.decode(enc or "utf-8", errors="replace"))
else:
out.append(val)
return "".join(out)
def safe_filename(name: str | None) -> str:
name = decode_mime_words(name) or "attachment.pdf"
name = re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip("._")
if not name.lower().endswith(".pdf"):
name += ".pdf"
return name or "attachment.pdf"
def upload_pdf(path: str) -> None:
subprocess.run([RMAPI_BIN, "put", path, RM_TARGET], check=True)
def move_to_trash(m: imaplib.IMAP4_SSL, uid: bytes) -> None:
m.uid("copy", uid, MAILBOX_TRASH)
m.uid("store", uid, "+FLAGS", r"(\Deleted)")
m.expunge()
def main() -> int:
log("rm-ingest: start")
log(f"rm-ingest: mailbox_in={MAILBOX_IN} trash={MAILBOX_TRASH} target={RM_TARGET}")
m = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT)
m.login(GMX_USER, GMX_PASS)
log("rm-ingest: imap login OK")
typ, _ = m.select(MAILBOX_IN)
log(f"rm-ingest: selected mailbox {MAILBOX_IN}")
if typ != "OK":
raise SystemExit(f"Cannot select mailbox {MAILBOX_IN}")
typ, data = m.uid("search", None, "ALL")
if typ != "OK":
raise SystemExit("Search failed")
uids = data[0].split()
log(f"rm-ingest: found={len(uids)} messages in {MAILBOX_IN}")
if not uids:
m.logout()
return 0
for uid in uids:
log(f"rm-ingest: processing uid={uid.decode(errors='ignore')}")
typ, msgdata = m.uid("fetch", uid, "(RFC822)")
if typ != "OK" or not msgdata or msgdata[0] is None:
continue
raw = msgdata[0][1]
msg = email.message_from_bytes(raw, policy=policy.default)
pdf_paths: list[str] = []
with tempfile.TemporaryDirectory() as td:
for part in msg.walk():
if part.is_multipart():
continue
ctype = (part.get_content_type() or "").lower()
fname = part.get_filename()
payload = part.get_payload(decode=True)
if not payload:
continue
size = len(payload)
if size == 0 or size > 25 * 1024 * 1024:
log(f"rm-ingest: skip part (size={size} bytes)")
continue
is_pdf = (
ctype == "application/pdf"
or (fname and fname.lower().endswith(".pdf"))
)
if not is_pdf:
continue
out = os.path.join(td, safe_filename(fname))
base, ext = os.path.splitext(out)
n = 1
while os.path.exists(out):
out = f"{base}_{n}{ext}"
n += 1
with open(out, "wb") as f:
f.write(payload)
pdf_paths.append(out)
if not pdf_paths:
log(f"rm-ingest: uid={uid.decode(errors='ignore')} no pdf -> trash")
move_to_trash(m, uid)
continue
try:
for p in pdf_paths:
log(f"rm-ingest: uploading {os.path.basename(p)} -> {RM_TARGET}")
upload_pdf(p)
log("rm-ingest: upload OK")
move_to_trash(m, uid)
log(f"rm-ingest: uid={uid.decode(errors='ignore')} moved to trash")
except subprocess.CalledProcessError as e:
log(f"rm-ingest: rmapi failed (code={e.returncode}) -> FAILED")
continue
m.logout()
return 0
if __name__ == "__main__":
exit(main())