Emailing files to reMarkable paper tablet

Setup using rmapi + postfix + reMarkable cloud

The reMarkable e-ink tablet is primarily meant to serve as a replacement for paper notebooks. It makes for a great PDF reader with things like technical manuals or scientific papers. While the tablet offers an outstanding screen and writing experience, the software is somewhat lacking. This post will summarize a setup I implemented to transfer PDF files to my reMarkable by emailing them to myself.

Existing transfer options

The easiest way to transfer files to the reMarkable is by using the desktop application. However, it is not currently officially available for Linux. There is also a mobile app for file transfer, but that then involves the extra step of sending the file I want to my phone. There’s also the option of transfer via USB, which uses a simple Web server built into the reMarkable. This is a good method but of course requires the USB cable to be connected.

Transfer over email

A transfer option that I would like is to just email a file to a certain address, and have it show up on my reMarkable. Amazon Kindle has this feature and it is very convenient. There is nothing like that for the reMarkable, but it turns out to be quite easy to implement relying on some existing software.

My setup consists of the following:

  1. reMarkable tablet, with cloud services enabled.

  2. A Postfix email server.

  3. rmapi installed on the server, which is a Go application for using the reMarkable cloud API, written by Javier Uruen Val.

  4. A simple Python script to process emails sent to an email address I allocate for the reMarkable.

Server setup

On the server, you need to install rmapi first, by following the instructions on Github. In my case, with golang already installed, it was sufficient to run go get -u github.com/juruen/rmapi.

The server’s rmapi installation needs to be connected to the cloud, so you have to run rmapi once and input the one-time code from my.remarkable.com. This is a one-time operation.

After that, you need to create an email address for the reMarkable tablet, and have it forward to a script. Configuration details depend on the server. I chose remarkable -at- umanovskis (dot) se as the address, so the Postfix configuration looks like this:

First, a new virtual entry in /etc/postfix/virtual (email written properly using the @ sign):

remarkable -at- umanovskis.se remarkable-alias

Then the remarkable-alias in turn forwards to a script. This is because virtualdb entries cannot directly forward to a script, so the two steps are needed. To make it cleaner, I don’t add the alias globally but put it in my ~/.aliases, as follows

remarkable-alias: "|/home/duman/remarkable-mail.py --authorized-senders-only"

Done like this, the user-specific aliases file has to be included in alias_maps of Postfix main.cf as well. After running postmap && newaliases and reloading Postfix, there’s a new email address that forwards to a Python script.

Python script

With the server configured, the remainder of the task is to have the Python script that will receive emails, check for pdf attachments and upload them via rmapi. I am not very strong at Python so the resulting code is not particulary “Pythonic”, but serves the purpose well for a late-night hack.

#!/usr/bin/python

import sys, subprocess
from os.path import expanduser
from datetime import datetime
import email
import email.utils

class RemarkableMailer:
    
    def __init__(self):
        self.loglines = []
        self.homedir = expanduser("~")
        self.logfile = self.homedir + "/remarkable/mailer.log"
        self.authorized_senders = []
        self.validate_senders = False
        self.input = ""
        self.rmapi = self.homedir + "/go/bin/rmapi"

    def log(self, message):
        timestamp = datetime.now().strftime('[%d-%m-%Y %H:%M] ')
        self.loglines.append(timestamp + message + "\n")

    def write_log(self):
        try:
            with open(self.logfile, 'a') as f:
                f.writelines(self.loglines)
        except Exception as e:
            print e

    def load_authorized_senders(self):
        try:
            with open(self.homedir + "/.authorized_script_senders", "r") as scripts_file:
                authorized_senders = [l.rstrip() for l in scripts_file.readlines() if not l.startswith('#')]
        except IOError as e:
            self.log(e)


    def run(self, argv):
        if "--authorized-senders-only" in argv:
            self.log("Running with whitelist")
            self.load_authorized_senders()
            self.validate_senders = True

        self.input = sys.stdin.read()
        if len(self.input) > 0:
            email, sender = self.process_input()
            if email is not None:
                valid_sender = self.validate_senders and sender in self.auhtorized_senders
                if valid_sender or self.validate_senders == False:
                    self.log ("Processing email from " + sender[0])
                    filelist = self.extract_pdfs(email)
                    self.upload(filelist)
                else:
                    self.log ("Ignoring unauthorized sender " + sender)

    def save_raw(self, data):
        try:
            raw = open(self.homedir + "/remarkable/raw-email", "w")
            raw.write(data)
            raw.close()
        except Exception:
            pass

    def process_input(self):
        try:
            # Enable for debug
            #self.save_raw(self.input)
            msg = email.message_from_string(self.input)
            sender = msg.get_all('from')
            return (msg, email.utils.parseaddr(sender))
        except Exception as e:
            self.log(str(e))
            return (None, None)

    def extract_pdfs(self, email):
        saved_files = []
        if email.is_multipart():
            for part in email.get_payload():
                if part.get_content_type() == "application/pdf":
                    payload = part.get_payload(decode=True)
                    filename = part.get_filename()
                    self.save_pdf(filename, payload)
                    saved_files.append(filename)
        return saved_files
                        
    def save_pdf(self, filename, contents):
        with open(self.homedir + "/remarkable/" + filename, "w") as f:
            try:
                f.write(contents)
                self.log ("Saved " + filename)
            except Exception as e:
                self.log ("Failed writing " + filename)

    def upload(self, files):
        for f in files:
            path = self.homedir + "/remarkable/" + f
            self.log("Uploading " + path + " to tablet")
            proc = subprocess.call([self.rmapi, "put", path], shell=False, timeout=20)
            subprocess.call(["rm", path])


if __name__ == "__main__":
    mailer = RemarkableMailer()
    mailer.run(sys.argv)
    mailer.write_log()

The script attempts to parse standard input as an email and, in the case of success, looks for PDF attachments (identifying them by the application/pdf MIME type), temporarily saves them to the file system and calls rmapi to upload to the reMarkable cloud. I also verify that the email’s sender is in a whitelist, which is a neat detail I like for scripts that can be triggered by email. It’s not a serious security solution, but prevents script activation by spambots that could scrape the email from somewhere.

As a result, this setup allows me to simply email a PDF file, and have it show up on the reMarkable as soon as the tablet has a Wifi connection available.

 
comments powered by Disqus