cnoceda.com

Delta Chat and Org-Mode

English

Hello everyone,

Today I’d like to share some thoughts about Delta Chat, a decentralized and encrypted messaging system. I’ve been testing it for many months, in different contexts. I’ll share my conclusions later on.

But first, I want to give some context about how I see the current messaging landscape — with its countless options — and how these mix with family, social, and work life.

I’ve been reflecting on privacy for quite some time — sometimes even in a slightly paranoid way — and along the way I came across a phrase that really stuck with me:

"Privacy is the power to selectively reveal oneself to the world."
— Eric Hughes, The Cypherpunk Manifesto (1993)

This quote perfectly sums up what privacy means to me. It’s not about hiding from the world. We all have responsibilities: family, work, taxes… there are things we inevitably have to share. But there are also parts of our lives we prefer to keep private, simply because they’re no one else’s business.

From this perspective, it’s not about completely eliminating WhatsApp, Teams, or even social networks (well… maybe those). It’s more about having options. Being able to choose tools that let us have truly private conversations, whenever and with whomever we want.

Because in the end, nothing is purely black or white. Everything has shades of gray, and privacy shouldn’t be the exception.

The same goes for the so-called “messaging app wars.” Which one should you choose? Well, as always — it depends. In my case, for work I use Teams and WhatsApp. For my hobbies I use Telegram. And for family and private matters, I use Deltachat.

Someone might say: “That’s a lot of apps! I just use one.” I’ve thought about that too, but consolidation is a myth — and let the first stone be cast by anyone who only has one social media app, or one banking app, or one restaurant loyalty app… Why should messaging be any different?

Here’s a great article about this excellent app: Everything You Think You Know About DeltaChat Is Wrong – Makefile.feld

Why Deltachat?

Here are the main reasons:

That’s the good things.

What about the bad ones?

Like any software still in active development, there are things to improve. For example, public groups — there’s little control over who joins.

Using email accounts on standard servers (Fastmail, Gmail, etc.) — i.e., those not configured for Deltachat — results in delays in push notifications. I used this setup for a while and noticed it. While the rest of my family got messages almost instantly, I had to refresh the app or I’d miss them entirely. I’d say Deltachat isn’t quite instant messaging.

It’s not a big deal for me, but video/audio calls are still in development. Multimedia (voice messages, photos, and videos) works fine, though there are issues with large video files.

What other geeky things can you do with Deltachat?

One of the most useful features is creating bots to help you with tasks. For example, I use one to manage my tasks.

On Mastodon I met @thibaut, who built a bot that takes messages and stores them in an org file. The article Deltachat inbox bot explains how he uses it and includes a link to the code.

Inside the repository there’s a file called org-commands.el, which contains the function the bot calls for each incoming message. The data is passed to Emacs from the bot (written in c):

emacs --batch \
        --eval \'(load-file "./org-commands.el\")\' \
        --eval=\'(setq attachment-filename \"%s\")\' \
        /org/inbox.org -f capture-message

This launches an Emacs instance, loads the org-commands.el file, passes the attached filename (if any) into attachment-file, opens the /org/inbox.org file, and runs the capture-message function.

All of this runs in a container, which needs a specific configuration outside the scope of this article. The most important part here is org-commands.el:

(require 'org)
(require 'org-attach)

(defun capture-message ()
  (let* ((media-filename
          (and (not (string= attachment-filename ""))
               attachment-filename))
         (task-string (concat "* [ ] "
                              (when media-filename "ATTACHMENT ")
                              (with-temp-buffer
                                (insert-file-contents "/deltachat-db/msg_content.txt")
                                (buffer-string))
                              "\ncreated "))
         make-backup-files)

    (goto-char (point-max))
    (insert task-string)
    (org-time-stamp-inactive '(16))

    (when media-filename
      (let ((org-attach-method 'mv)) (org-attach-attach media-filename)))

    (save-buffer)))

How does it help my workflow?

I saw huge potential here. By slightly tweaking the bot, I could tailor it to my needs:

To do this, the message needs to follow a certain format:

Let’s break this down.

Initial setup

We load the modules we need. Emacs starts fresh each time.

(require 'org)
(require 'org-attach)
(require 'url)
(require 'xml)

Not sure if this is strictly necessary, but at first I had issues with special characters.

(prefer-coding-system       'utf-8)
(set-default-coding-systems 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
(setq default-buffer-file-coding-system 'utf-8)

I set the directory for storing attachments inside the container.

(setq org-attach-id-dir "/attach/")

capture-message function

This is the modified version of the original function:

(defun capture-message ()
  (let* ((media-filename
          (and (not (string= attachment-filename ""))
               attachment-filename))
         (msg-content (split-first-line
                       (with-temp-buffer
                         (insert-file-contents "/deltachat-db/msg_content.txt")
                         (buffer-string))))
         (msg-date (or message-date 0))
         (task-string (capture-template msg-content media-filename msg-date))
         make-backup-files)

    (goto-char (point-max))
    (insert task-string)

    (when media-filename
      (let ((org-attach-method 'mv)) (org-attach-attach media-filename)))

    (save-buffer)))

split-first-line function

Splits the received text into two elements: the first line and the rest of the message.

(defun split-first-line (text)
  "Split TEXT into two parts: the first line and the rest of the text.
Returns a list with (first-line rest-of-text)."
  (if (string-match "\n" text)
      (list (substring text 0 (match-beginning 0))  ;; First line
            (substring text (match-end 0)))         ;; Rest of the text
    (list text ""))) ;; If no newline, rest is empty

get-tags function

Formats the given text into orgmode-style tags, turning each word into a tag.

(defun get-tags-data (tags)
  (let ((otags (mapconcat 'identity (delete "" (split-string tags " ")) ":")))
    (message "%s" otags)
    (if (not (string= otags ""))
        (format " :%s:" otags)
      "")))

capture-template function

Returns the formatted text for the org file entry. It takes three arguments:

(defun capture-template (msg attach msg-timestamp)
  "Build the templates for the different choices.
1. if beging with http, gets the link, and the second line are the tags space separted.
2. If first line begin with daily capture a Daily entry.
   Gets the rest of the line as tags for the heading.
   The second line as head of the entry.
   The rest as the content of the journal entry.
3. Any other message is a TODO Task."
  (let* ((first-line (car msg))
         (other-lines (cadr msg))
         (create-date (format-time-string "[%Y-%m-%d %a %H:%M]\n" msg-timestamp "Europe/Madrid"))
         web-data
         task-txt
         url-title
         url-description
         tags)
    (cond
     ((and (length> first-line 6) (string= "http" (downcase (substring-no-properties first-line 0 4))))
      (setq web-data (get-url-data first-line)
            url-title (or (cdr (assoc 'title web-data)) "No title")
            url-description (cdr (assoc 'description web-data))
            tags (get-tags-data other-lines))
      (concat "* "
              url-title
              tags
              "\n:PROPERTIES:\n:CREATED: " create-date
              ":WEBLINK: " first-line "\n"
              ":END:\n"
              "[[" first-line "][" url-title "]]\n"
              (when url-description (concat "#+begin_quote\n" url-description "\n#+end_quote\n"))
              ))
     ((and (length> first-line 6) (string= "daily" (downcase (substring-no-properties first-line 0 5))))
      (setq tags (and (length> first-line 6) (get-tags-data (substring-no-properties first-line 6)))
            msg (split-first-line other-lines))
      (concat "* " (format-time-string "%H:%M " msg-timestamp "Europe/Madrid")
              (car msg) tags
              "\n:PROPERTIES:\n:CREATED: " create-date
              ":END:\n"
              (cadr msg)))
     (t
      (concat "* TODO "
              first-line
              (when attach ":ATTACH:")
              "\n:PROPERTIES:\n:CREATED: " create-date
              ":END:\n"
              other-lines)))
    ))

Special functions for documenting web links

These functions don’t currently work properly for me, but I use them in my workflow to complete the information before classifying a link.

With get-url-data, we create a buffer with the url content so we can search for the necessary info — usually the title and the description meta tag, if present.

(defun get-url-data (url)
  (let ((data '())
        (web-buffer (condition-case nil
                        (url-retrieve-synchronously url t)
                      (error nil))))
    (when web-buffer
      (with-current-buffer web-buffer
        (push (search-info "<title>" nil) data)
        (push (search-info "<meta name=[\"]?description[\"]? " t) data)))
    data))

With search-info, we look for data inside the buffer. The approach is to place the cursor right before what we need and copy the characters until the end of the <title> or meta tag. We differentiate because the closing tags are different.

(defun search-info (str meta)
  "Search STR tag and get the content, if META is not-nil, gets content attributte of the tag. "
  (let ((description nil)
        beg-tag end-tag)
    (goto-char (point-min))
    (setq beg-tag (search-forward-regexp str nil t))

    (when beg-tag
      (cond
       (meta
        (let* ((end-tag (search-forward ">" nil t))
               (full-tag (buffer-substring-no-properties (- beg-tag (length str)) end-tag))
               (parse-tag (libxml-parse-html-region (- beg-tag (length str)) end-tag))
               )
          (setq description (cons 'description (dom-find-meta-description  parse-tag)))))
       (t
        (setq end-tag (1- (search-forward "<" nil t)))
        (setq description (cons 'title (decode-entities (buffer-substring-no-properties beg-tag end-tag))))))
      )))

Function to re-encode special characters.

(defun decode-entities (html)
  (with-temp-buffer
    (save-excursion (insert html))
    (xml-parse-string)))

Helper function for meta search using the dom module

(defun dom-find-meta-description (tree)
  "Finds the content of <meta name=\"description\"> using dom.el."
  (let ((meta (dom-by-tag tree 'meta)))
    (when-let ((meta-node (seq-find (lambda (el)
                                      (string= (dom-attr el 'name) "description"))
                                    meta)))
      (dom-attr meta-node 'content))))

Conclusions

I think I’m going to stick with Deltachat and try to bring as many people on board as I can. Yes, it’s still in development, but the peace of mind knowing my messages are only read by the intended recipients is worth it.

By the way, one of the conversations I had with my kids was: “Another app, Dad?!” — which ended quickly when I grabbed their phones and found the 200 apps they have. Among them: Snapchat, Instagram, WhatsApp… all doing basically the same thing. I don’t think one more will hurt.

The integration I now have with org-mode is something I’ve never had with any other app. In the future, I’d like to be able to send messages from Emacs as well. But that’s another story.

Note: This time I translated with some help, so please forgive me if it’s not perfect.

Author: Unknown

Date: 2025-06-15

Emacs 30.1 (Org mode 9.7.11)