🌙 Dark Mode ☀️

← Zur Übersicht

How to's →
Teil I: Mail to reMarkable
PDFs aus E-Mails auf das reMarkable

PDF-Übertragung zum reMarkable per E-Mail/GMX
(automatisiert, ähnlich "reMailable" / "Send to Kindle")

1. Stand 12/2025:
Ggfs. ist künftige neuere Software zu verwenden, insbes. betreffend rmapi (derzeit 0.0.29)
2. SICHERHEITSHINWEIS:
Nutze immer ein separates GMX-Konto + App-Passwort (nicht dein Hauptkonto-Passwort)!

PDF auf das reMarkable per GMX und rmapi transferieren

📋 A. Voraussetzungen

🔧 B. Einrichtung

1. GMX-Konto vorbereiten

1

2. Ubuntu Konfiguration

2.1 Zeitzone
sudo timedatectl set-timezone Europe/Berlin
timedatectl status
2.2 Python3 prüfen
which python3
2.3 Go 1.23.5 (richtige Architektur wählen!)+ rmapi installieren
sudo apt update
wget https://go.dev/dl/go1.23.5.linux-amd64.tar.gz
sudo rm -rf /usr/local/go sudo tar -C /usr/local -xzf go1.23.5.linux-amd64.tar.gz
export PATH=/usr/local/go/bin:$PATH
go version

go install github.com/ddvk/rmapi@v0.0.29
sudo install -m 0755 ~/go/bin/rmapi /usr/local/bin/rmapi
rmapi version
2.4 rm_ingest User + rmapi auth
sudo adduser --disabled-password --gecos "" rm_ingest
sudo -iu rm_ingest rmapi
2.5 Python-Script installieren (Script siehe unter C.)
sudo mkdir -p /opt/rm-ingest
sudo nano /opt/rm-ingest/gmx_to_rm.py
2.6 Environment + systemd
sudo chmod 0755 /opt/rm-ingest/gmx_to_rm.py
sudo mkdir -p -m 0700 -o rm_ingest -g rm_ingest /etc/rm-ingest
sudo nano /etc/rm-ingest/gmx.env
sudo chmod 0600 /etc/rm-ingest/gmx.env
sudo chown rm_ingest:rm_ingest /etc/rm-ingest/gmx.env

2.6.1 /etc/rm-ingest/gmx.env (ERSETZE DURCH DEINE DATEN!)
				GMX_USER="deine-email@gmx.de"
				GMX_PASS="dein-app-passwort"
2.6.2 /etc/systemd/system/rm-ingest.service
[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
2.6.3 /etc/systemd/system/rm-ingest.timer
[Unit]
Description=GMX to reMarkable every 5min (07-23)

[Timer]
OnCalendar=*-*-* 07..22:55/5:00
Persistent=true

[Install]
WantedBy=timers.target
2.7 Service aktivieren + Cron
sudo systemctl daemon-reload
sudo systemctl enable --now rm-ingest.timer
sudo systemctl list-timers | grep rm-ingest

echo "0 2 * * 0 rmapi ls / > /dev/null 2>&1" | sudo crontab -u rm_ingest -

🐍 C. Vollständiges Python-Script

🆓 Info: Nachfolgendes Skript steht für private, nicht-gewerbliche Nutzung kostenlos zur Verfügung. Kommerzielle Nutzung oder Weiterverbreitung mit Änderungen erfordert eine ausdrückliche Genehmigung.

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())

✅ D. Testen

3 Manuell testen
sudo systemctl start rm-ingest.service
sudo journalctl -u rm-ingest.service -n 50 --no-pager
4 Automatik testen
  • E-Mail mit PDF-Anhang an deine GMX-Adresse senden
  • Warte 5 Minuten (Timer-Intervall)
  • Prüfe reMarkable Root-Verzeichnis
5 Status überwachen
sudo systemctl status rm-ingest.timer
sudo journalctl -u rm-ingest.service -f # Live-Logs
Teil I fertig! PDFs aus Mails sollten nun automatisch im reMarkable Root-Verzeichnis landen.
Timer läuft alle 5 Min (07:00-22:55), Ordner bleiben sauber, Logs rotieren automatisch.