2022-07-09 13:25:32 +02:00
|
|
|
;; SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
;; SPDX-FileCopyrightText: Copyright (C) 2022 by sterni
|
|
|
|
|
2022-01-13 00:33:59 +01:00
|
|
|
(in-package :note)
|
2021-08-02 15:13:05 +02:00
|
|
|
(declaim (optimize (safety 3)))
|
|
|
|
|
|
|
|
;;; util
|
|
|
|
|
2022-02-01 16:33:54 +01:00
|
|
|
;; TODO(sterni): merge this with mblog::*copy-buffer-size*
|
|
|
|
(defvar *copy-buffer-size* 4096)
|
|
|
|
|
2021-08-02 15:13:05 +02:00
|
|
|
(defun html-escape-stream (in out)
|
|
|
|
"Escape characters read from stream IN and write them to
|
2022-02-01 16:33:54 +01:00
|
|
|
stream OUT escaped using WHO:ESCAPE-STRING-MINIMAL."
|
|
|
|
(let ((buf (make-string *copy-buffer-size*)))
|
|
|
|
(loop for len = (read-sequence buf in)
|
|
|
|
while (> len 0)
|
|
|
|
do (write-string (who:escape-string-minimal (subseq buf 0 len)) out))))
|
2021-08-02 15:13:05 +02:00
|
|
|
|
|
|
|
(defun cid-header-value (cid)
|
|
|
|
"Takes a Content-ID as present in Apple Notes' <object> tags and properly
|
|
|
|
surrounds them with angle brackets for a MIME header"
|
|
|
|
(concatenate 'string "<" cid ">"))
|
|
|
|
|
2022-01-13 00:33:59 +01:00
|
|
|
(defun find-mime-message-date (message)
|
2022-02-01 00:01:59 +01:00
|
|
|
(when-let ((date-string (car (mime:mime-message-header-values "Date" message))))
|
2022-01-13 00:33:59 +01:00
|
|
|
(date-time-parser:parse-date-time date-string)))
|
|
|
|
|
2021-08-02 15:13:05 +02:00
|
|
|
;;; main implementation
|
|
|
|
|
2022-01-13 00:33:59 +01:00
|
|
|
(defun apple-note-mime-subtype-p (x)
|
|
|
|
(member x '("plain" "html") :test #'string-equal))
|
|
|
|
|
|
|
|
(deftype apple-note-mime-subtype ()
|
|
|
|
'(satisfies apple-note-mime-subtype-p))
|
|
|
|
|
|
|
|
(defclass apple-note (mime:mime-message)
|
|
|
|
((text-part
|
|
|
|
:type mime:mime-text
|
|
|
|
:initarg :text-part
|
|
|
|
:reader apple-note-text-part)
|
|
|
|
(subject
|
|
|
|
:type string
|
|
|
|
:initarg :subject
|
|
|
|
:reader apple-note-subject)
|
|
|
|
(uuid
|
|
|
|
:type string
|
|
|
|
:initarg :uuid
|
|
|
|
:reader apple-note-uuid)
|
|
|
|
(time
|
|
|
|
:type integer
|
|
|
|
:initarg :time
|
|
|
|
:reader apple-note-time)
|
|
|
|
(mime-subtype
|
|
|
|
:type apple-note-mime-subtype
|
|
|
|
:initarg :mime-subtype
|
|
|
|
:reader apple-note-mime-subtype))
|
|
|
|
(:documentation
|
2022-01-28 15:49:16 +01:00
|
|
|
"Representation of a Note created using Apple's Notes via the IMAP backend"))
|
2022-01-13 00:33:59 +01:00
|
|
|
|
2021-08-02 15:13:05 +02:00
|
|
|
(defun apple-note-p (msg)
|
|
|
|
"Checks X-Uniform-Type-Identifier of a MIME:MIME-MESSAGE
|
2022-01-13 00:33:59 +01:00
|
|
|
to determine if a given mime message claims to be an Apple Note."
|
2022-02-01 00:01:59 +01:00
|
|
|
(when-let (uniform-id (car (mime:mime-message-header-values
|
|
|
|
"X-Uniform-Type-Identifier"
|
|
|
|
msg)))
|
|
|
|
(string-equal uniform-id "com.apple.mail-note")))
|
2022-01-27 16:06:28 +01:00
|
|
|
|
2022-01-13 00:33:59 +01:00
|
|
|
(defun make-apple-note (msg)
|
|
|
|
(check-type msg mime-message)
|
|
|
|
|
|
|
|
(unless (apple-note-p msg)
|
|
|
|
(error "Passed message is not an Apple Note according to headers"))
|
|
|
|
|
|
|
|
(let ((text-part (mime:find-mime-text-part msg))
|
2022-02-01 00:01:59 +01:00
|
|
|
(subject (car (mime:mime-message-header-values "Subject" msg :decode t)))
|
|
|
|
(uuid (when-let ((val (car (mime:mime-message-header-values
|
|
|
|
"X-Universally-Unique-Identifier"
|
|
|
|
msg))))
|
2022-01-13 00:33:59 +01:00
|
|
|
(string-downcase val)))
|
|
|
|
(time (find-mime-message-date msg)))
|
|
|
|
;; The idea here is that we don't need to check a lot manually, instead
|
|
|
|
;; the type annotation are going to do this for us (with sufficient safety?)
|
|
|
|
(change-class msg 'apple-note
|
|
|
|
:text-part text-part
|
|
|
|
:subject subject
|
|
|
|
:uuid uuid
|
|
|
|
:time time
|
|
|
|
:mime-subtype (mime:mime-subtype text-part))))
|
|
|
|
|
|
|
|
(defgeneric apple-note-html-fragment (note out)
|
|
|
|
(:documentation
|
|
|
|
"Takes an APPLE-NOTE and writes its text content as HTML to
|
|
|
|
the OUT stream. The <object> tags are resolved to <img> which
|
|
|
|
refer to the respective attachment's filename as a relative path,
|
|
|
|
but extraction of the attachments must be done separately. The
|
|
|
|
surrounding <html> and <body> tags are stripped and <head>
|
|
|
|
discarded completely, so only a fragment which can be included
|
|
|
|
in custom templates remains."))
|
|
|
|
|
|
|
|
(defmethod apple-note-html-fragment ((note apple-note) (out stream))
|
|
|
|
(let ((text (apple-note-text-part note)))
|
2021-08-02 15:13:05 +02:00
|
|
|
(cond
|
|
|
|
;; notemap creates text/plain notes we need to handle properly.
|
|
|
|
;; Additionally we *could* check X-Mailer which notemap sets
|
2022-01-13 00:33:59 +01:00
|
|
|
((string-equal (apple-note-mime-subtype note) "plain")
|
2021-08-02 15:13:05 +02:00
|
|
|
(html-escape-stream (mime:mime-body-stream text :binary nil) out))
|
|
|
|
;; Notes.app creates text/html parts
|
2022-01-13 00:33:59 +01:00
|
|
|
((string-equal (apple-note-mime-subtype note) "html")
|
2021-08-02 15:13:05 +02:00
|
|
|
(closure-html:parse
|
|
|
|
(mime:mime-body-stream text)
|
|
|
|
(make-instance
|
|
|
|
'apple-note-transformer
|
|
|
|
:cid-lookup
|
|
|
|
(lambda (cid)
|
2022-01-13 00:33:59 +01:00
|
|
|
(when-let* ((part (mime:find-mime-part-by-id note (cid-header-value cid)))
|
2021-08-02 15:13:05 +02:00
|
|
|
(file (mime:mime-part-file-name part)))
|
|
|
|
file))
|
|
|
|
:next-handler
|
|
|
|
(closure-html:make-character-stream-sink out))))
|
2022-01-13 00:33:59 +01:00
|
|
|
(t (error "Internal error: unexpected MIME subtype")))))
|