-
Notifications
You must be signed in to change notification settings - Fork 60
/
org-msg.el
1575 lines (1441 loc) · 59.7 KB
/
org-msg.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; org-msg.el --- Org mode to send and reply to email in HTML. -*- lexical-binding: t; -*-
;; Copyright (C) 2018-2021 Jérémy Compostella
;; Author: Jérémy Compostella <jeremy.compostella@gmail.com>
;; Created: January 2018
;; Keywords: extensions mail
;; Homepage: https://github.com/jeremy-compostella/org-msg
;; Package-Version: 4.0
;; Package-Requires: ((emacs "24.4") (htmlize "1.54"))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; OrgMsg is a GNU/Emacs global minor mode mixing up Org mode and your
;; Mail User Agent Mode to compose and reply to emails in a HTML
;; friendly style.
;;; Code:
(require 'cl-lib)
(require 'cl-macs)
(require 'cl-seq)
(require 'gnus-art)
(require 'gnus-cite)
(require 'gnus-dired)
(require 'gnus-icalendar)
(require 'gnus-msg)
(require 'htmlize)
(require 'message)
(require 'mml)
(require 'org)
(require 'ox)
(require 'subr-x)
(require 'url-parse)
(require 'xml)
(defgroup org-msg nil
"Org Message group."
:group 'applications)
(defvar org-msg-attachment '()
"Temporary variable to pass the list of attachment.")
(defvar org-msg-mml nil
"Temporary variable to pass the MML content.")
(defvar org-msg-alternatives nil
"Temporary alist to hold the contents of each alternative.")
(defvar org-msg-export-in-progress nil
"Internal use only.
It is used by function advice.")
(defvar-local org-msg-mml-buffer-list '()
"Used to store the `mml-buffer-list' variable content of the
current message. `mml-buffer-list' is the list of temporary
buffer holding mml contents.")
(defcustom org-msg-separator "--citation follows this line (read-only)--"
"String separating the reply area and the original mail."
:type '(string))
(defcustom org-msg-options "html-postamble:nil toc:nil author:nil email:nil"
"Org Mode #+OPTIONS."
:type '(string))
(defcustom org-msg-startup nil
"Org Mode #+STARTUP."
:type '(string))
(defcustom org-msg-alternative-exporters
`((text . ("text/plain" . ,(apply-partially 'org-msg-export-as-text 'ascii)))
(utf-8 . ("text/plain" . ,(apply-partially 'org-msg-export-as-text 'utf-8)))
(org . ("text/org" . identity))
(html . ("text/html" . org-msg-export-as-html)))
"Alist of the available alternative exporters.
Entries are in the form of `(tag . (mime-part . export-function))'.
The export function takes an `org-msg' message buffer string and
returns the exported content as a string."
:type '(list (const symbol (cons string symbol))))
(defcustom org-msg-default-alternatives '((new . (html))
(reply-to-html . (html)))
"Alternative MIME formats to send.
This customization variable orderly lists the alternatives of an
outgoing email. The possible keys are:
- `new' for new email is not a reply
- `reply-to-text' when the email being replied to is plain text
- `reply-to-html' when the email being replied to is html
When set to a simple list of alternatives and for backward
compatibility it applies to new emails and replies to html emails
but not to replies to plain text emails.
Available alternatives are listed in `org-msg-alternative-exporters'."
:type '(choice (list symbol)
(alist :key-type symbol :value-type (repeat symbol))))
(defcustom org-msg-greeting-fmt nil
"Mail greeting format.
If it contains a '%s' format, '%s' is replaced with the first
name of the person you are replying to with a space prefix.
Example: \"Hi%s,\"
is replaced by either \"Hi Mark,\" or \"Hi,\"."
:type '(string))
(defcustom org-msg-recipient-names '()
"List of recipients preferred names.
The automatic replacement of '%s' format in
`org-msg-greeting-fmt' is not always ideal. Some email addresses
do not include the actual recipient name or this recipient wants
to be called with another name, an acronym or its name has
accents. This variable can be used to specify these exceptions.
Example: ((\"jeremy.compostella@gmail.com\" . \"Jérémy\"))"
:type '(list (cons string string)))
(defcustom org-msg-greeting-name-limit 1
"Maximum number of recipient first name for the greeting format.
If replying to an email for which the 'To' field contains more
than one recipient and the `org-msg-greeting-fmt' contains a '%s'
format, this variable limits the number of recipient first name
used as a replacement of the '%s' format. nil means unlimited."
:type '(integer))
(defcustom org-msg-greeting-fmt-mailto nil
"Define the format behavior for recipient greeting.
If t and `org-msg-greeting-fmt' contains a '%s' the recipient
name is formatted as a mailto link."
:type '(boolean))
(defcustom org-msg-signature nil
"Mail signature string appended if not nil.
The part in the signature block gets applied the \"signature\"
CSS style.
Example:
\"\n\nRegards,\n\n#+begin_signature\n-- *Your name*\n#+end_signature\""
:type '(string))
(defcustom org-msg-posting-style 'top-posting
"Define the posting style for HTML replies.
Can be either `top-posting' or nil."
:type '(symbol))
(defcustom org-msg-undesirable-headers '("^attachments?$")
"List of undesirable header to delete from the original email."
:type '(list regexp))
(defcustom org-msg-dnd-protocol-alist
'(("^file:" . org-msg-dnd-handle-file))
"The functions to call when a file drop is made."
:type '(repeat (cons (regexp) (function))))
(defcustom org-msg-attached-file-reference
"attach[a-z]*\\|enclose"
"Regular expression detecting a reference to an attached file."
:type 'regexp)
(defun org-msg-lighten (color)
"Make a color lighter by a 20%."
(apply 'color-rgb-to-hex
(append
(apply 'color-hsl-to-rgb
(apply 'color-lighten-hsl
(append
(apply 'color-rgb-to-hsl
(color-name-to-rgb color))
(list 20))))
(list 2))))
(defconst org-msg-default-style
(let* ((font-family '(font-family . "\"Arial\""))
(font-size '(font-size . "10pt"))
(font `(,font-family ,font-size))
(line-height '(line-height . "10pt"))
(bold '(font-weight . "bold"))
(theme-color "#0071c5")
(color `(color . ,theme-color))
(table `(,@font (margin-top . "0px")))
(ftl-number `(,@font ,color ,bold (text-align . "left")))
(inline-modes '(asl c c++ conf cpp csv diff ditaa emacs-lisp
fundamental ini json makefile man org plantuml
python sh xml))
(inline-src `((color . ,(face-foreground 'default))
(background-color . ,(face-background 'default))))
(code-src
(mapcar (lambda (mode)
`(code ,(intern (concat "src src-" (symbol-name mode)))
,inline-src))
inline-modes))
(base-quote '((padding-left . "5px") (margin-left . "10px")
(margin-top . "10px") (margin-bottom . "0")
(font-style . "italic") (background . "#f9f9f9")))
(quote-palette '("#324e72" "#6a3a4c" "#7a4900" "#ff34ff"
"#ff4a46" "#008941" "#006fa6" "#a30059"
"#ffdbe5" "#000000" "#0000a6" "#63ffac"))
(quotes
(mapcar (lambda (x)
(let ((c (nth x quote-palette)))
`(blockquote ,(intern (format "quote%d" (1+ x)))
(,@base-quote
(color . ,c)
(border-left . ,(concat "3px solid "
(org-msg-lighten c)))))))
(number-sequence 0 (1- (length quote-palette))))))
`((del nil (,@font (color . "grey") (border-left . "none")
(text-decoration . "line-through") (margin-bottom . "0px")
(margin-top . "10px") (line-height . "11pt")))
(a nil (,color))
(a reply-header ((color . "black") (text-decoration . "none")))
(div reply-header ((padding . "3.0pt 0in 0in 0in")
(border-top . "solid #e1e1e1 1.0pt")
(margin-bottom . "20px")))
(span underline ((text-decoration . "underline")))
(li nil (,@font ,line-height (margin-bottom . "0px")
(margin-top . "2px")))
(nil org-ul ((list-style-type . "square")))
(nil org-ol (,@font ,line-height (margin-bottom . "0px")
(margin-top . "0px") (margin-left . "30px")
(padding-top . "0px") (padding-left . "5px")))
(nil signature (,@font (margin-bottom . "20px")))
(blockquote quote0 ,(append base-quote '((border-left . "3px solid #ccc"))))
,@quotes
(code nil (,font-size (font-family . "monospace") (background . "#f9f9f9")))
,@code-src
(nil linenr ((padding-right . "1em")
(color . "black")
(background-color . "#aaaaaa")))
(pre nil ((line-height . "12pt")
,@inline-src
(margin . "0px")
(font-size . "9pt")
(font-family . "monospace")))
(div org-src-container ((margin-top . "10px")))
(nil figure-number ,ftl-number)
(nil table-number)
(caption nil ((text-align . "left")
(background . ,theme-color)
(color . "white")
,bold))
(nil t-above ((caption-side . "top")))
(nil t-bottom ((caption-side . "bottom")))
(nil listing-number ,ftl-number)
(nil figure ,ftl-number)
(nil org-src-name ,ftl-number)
(table nil (,@table ,line-height (border-collapse . "collapse")))
(th nil ((border . "1px solid white")
(background-color . ,theme-color)
(color . "white")
(padding-left . "10px") (padding-right . "10px")))
(td nil (,@table (padding-left . "10px") (padding-right . "10px")
(background-color . "#f9f9f9") (border . "1px solid white")))
(td org-left ((text-align . "left")))
(td org-right ((text-align . "right")))
(td org-center ((text-align . "center")))
(div outline-text-4 ((margin-left . "15px")))
(div outline-4 ((margin-left . "10px")))
(h4 nil ((margin-bottom . "0px") (font-size . "11pt")
,font-family))
(h3 nil ((margin-bottom . "0px") (text-decoration . "underline")
,color (font-size . "12pt")
,font-family))
(h2 nil ((margin-top . "20px") (margin-bottom . "20px")
(font-style . "italic") ,color (font-size . "13pt")
,font-family))
(h1 nil ((margin-top . "20px")
(margin-bottom . "0px") ,color (font-size . "12pt")
,font-family))
(p nil ((text-decoration . "none") (margin-bottom . "0px")
(margin-top . "10px") (line-height . "11pt") ,font-size
,font-family))
(div nil (,@font (line-height . "11pt"))))))
(defcustom org-msg-enforce-css org-msg-default-style
"Define how to handle CSS style:
- list - style definition: see `org-msg-default-style' for
example.
- string - path to a CSS file: same as t but use this file
definitions."
:type '(choice (file :must-match t)
(list (list symbol symbol
(alist :value-type string)))))
(defcustom org-msg-reply-header-class 'reply-header
"Default CSS class for reply header tags."
:type '(symbol))
(defcustom org-msg-convert-citation nil
"Activate the conversion of mail citation into quote blocks.
If t, lines matching the '^>+ ' regular expression are turned
into multi-level quote blocks before being passed to the Org mode
HTML export engine."
:type '(boolean))
(defcustom org-msg-supported-mua '((gnus-user-agent . "gnus")
(message-user-agent . "message")
(mu4e-user-agent . "mu4e")
(notmuch-user-agent . "notmuch"))
"Supported Mail User Agents."
:type '(alist :value-type string))
(defun org-msg-dnd-handle-file (uri _action)
"Attach a file to the current draft.
URI is the file to handle, ACTION is one of copy, move, link or
ask."
(when-let ((file (dnd-get-local-file-name uri t)))
(org-msg-attach-attach file)))
(defun org-msg-mua-call (sym &optional default &rest arg)
"Call the specific MUA function for SYM with ARG parameters.
If no function is defined for this MUA, the DEFAULT function
is called."
(let ((mua (assoc-default mail-user-agent org-msg-supported-mua)))
(if mua
(let ((fun (intern (format "org-msg-%s-%s" sym mua))))
(if (functionp fun)
(apply fun arg)
(when default
(apply default arg))))
(error "Backend not found"))))
(defun org-msg-mml-recursive-support ()
"Return t if mml has recursive html support.
Starting with Emacs 28, mml recursively searches for the
text/html part allowing multipart composition with HTML content
including images.
If this is not supported, OrgMsg places the text/html as a single
part and via an advice on the
`mml-expand-html-into-multipart-related` function, it modified
the mime data structure."
(fboundp 'mml-expand-all-html-into-multipart-related))
(defun org-msg-save-article-for-reply-gnus (&optional parts header)
"Export the currently visited `gnus-article-buffer' as HTML.
If parts is not nil, it exports in a file using the
`gnus-article-browse-html-parts' function otherwise, it uses the
`gnus-article-browse-html-article' function. If the current
article contains other HTML emails as attachments, the
`browse-url-browser-function' is called several times. We only
keep track of the first call which is usually the actual email we
want to reply to. Both `gnus-article-browse-html-article' and
`gnus-article-browse-html-parts' also extract all the inline
images. This function returns the absolute path of the HTML
file."
(let* ((browse-url-browser-function #'ignore)
(save (cl-copy-list gnus-article-browse-html-temp-list)))
(cl-letf (((symbol-function 'gnus-summary-show-article) #'ignore))
(save-window-excursion
(if parts
(gnus-article-browse-html-parts parts header)
(gnus-article-browse-html-article))))
(prog1 (cl-set-difference gnus-article-browse-html-temp-list save
:test 'string=)
(setq gnus-article-browse-html-temp-list save))))
(defun org-msg-save-article-for-reply-mu4e ()
"Export the currently visited mu4e article as HTML."
(let ((msg mu4e-compose-parent-message))
(with-temp-buffer
(insert-file-contents-literally
(mu4e-message-readable-path msg) nil nil nil t)
(rfc2047-decode-region (point-min) (point-max))
(let ((parts (mm-dissect-buffer t t))
(header (cl-loop for field in '("from" "to" "cc" "date" "subject")
when (message-fetch-field field)
concat (format "%s: %s\n" (capitalize field) it))))
(when (and (bufferp (car parts))
(stringp (car (mm-handle-type parts))))
(setf parts (list parts)))
(let ((gnus-article-buffer (current-buffer))
(gnus-article-mime-handles parts))
(prog1
(org-msg-save-article-for-reply-gnus parts header)
(mm-destroy-parts parts)))))))
(defmacro org-msg-with-original-notmuch-message (&rest body)
"Execute the forms in BODY with the replied notmuch message
buffer temporarily current."
(declare (indent 0))
(let ((id (make-symbol "id"))
(buf (make-symbol "buf")))
`(let ((,id (org-msg-message-fetch-field "in-reply-to")))
(save-window-excursion
(let* ((notmuch-show-only-matching-messages t)
(,buf (notmuch-show (format "id:%s" (substring ,id 1 -1)))))
(notmuch-show-view-raw-message)
(prog1 (progn ,@body)
(kill-buffer ,buf)
(kill-buffer)))))))
(defun org-msg-save-article-for-reply-notmuch ()
"Export the currently visited notmuch article as HTML."
(let (header parts)
(cl-flet ((get-field (field)
(when-let ((value (org-msg-message-fetch-field field)))
(concat (capitalize field) ": " value))))
(org-msg-with-original-notmuch-message
(let ((fields (mapcar #'get-field
'("from" "subject" "to" "cc" "date"))))
(setf header (mapconcat 'identity (delq nil fields) "\n")))
(setf parts (mm-dissect-buffer))
(unless (listp (car parts))
(setf parts (list parts))))
(with-temp-buffer
(let ((gnus-article-buffer (current-buffer))
(gnus-article-mime-handles parts))
(prog1 (org-msg-save-article-for-reply-gnus parts header)
(mm-destroy-parts parts)))))))
(defun org-msg-attrs-str (attr)
"Convert ATTR list of attributes into a string."
(cl-flet ((attr-str (x)
(concat " " (symbol-name (car x)) "=\""
(xml-escape-string (cdr x)) "\"")))
(if attr
(apply 'concat (mapcar #'attr-str attr))
"")))
(defun org-msg-xml-escape-string (string)
"Convert STRING into a string containing valid XML character data.
This is a reduction of `xml-escape-string' to work-around a bug
during email generation where ''' is turned into
'&apos;'."
(with-temp-buffer
(insert string)
(dolist (substitution '(("&" . "&")
("<" . "<")
(">" . ">")
("\"" . """)))
(goto-char (point-min))
(while (search-forward (car substitution) nil t)
(replace-match (cdr substitution) t t nil)))
(buffer-string)))
(defun org-msg-xml-to-str (xml)
"Convert the XML tree into a HTML string."
(cond ((and (listp xml) (equal xml '(p nil " ")))
"<o:p> </o:p>")
((and (listp xml) (equal xml '(p nil)))
"<o:p>\n</o:p>")
((stringp xml)
(replace-regexp-in-string " " " "
(org-msg-xml-escape-string xml)))
((eq (car xml) 'comment)
(format "<!--%s-->" (caddr xml)))
((eq (car xml) 'style)
(format "<style>%s</style>" (caddr xml)))
((cddr xml)
(format "<%s%s>%s</%s>" (symbol-name (car xml))
(org-msg-attrs-str (cadr xml))
(apply 'concat (mapcar 'org-msg-xml-to-str (cddr xml)))
(symbol-name (car xml))))
((format "<%s%s/>" (symbol-name (car xml))
(org-msg-attrs-str (cadr xml))))))
(defun org-msg-css-to-list ()
"Convert the current buffer CSS content into a list.
\((tag class ((prop1 . val1) ...)) ...)."
(let ((l))
(save-excursion
(goto-char (point-min))
(while (re-search-forward "\\([a-zA-Z0-9, -\\._]+\\) *{" nil t)
(let ((selectors (split-string (match-string 1) "," nil " +"))
(start (point))
(props '()))
(backward-char 1)
(forward-sexp)
(let ((text-props (buffer-substring start (1- (point)))))
(dolist (p (split-string text-props ";" t "[\n ]*"))
(cl-multiple-value-bind (prop val) (split-string p ":" t "[\n ]*")
(push (cons (intern prop) val) props)))
(dolist (sel selectors)
(cl-multiple-value-bind (tag class) (split-string sel "\\.")
(push (list (if (string= tag "") nil (intern tag))
(if (stringp class) (intern class) nil)
props)
l)))))))
l))
(defun org-msg-css-file-to-list (file)
"Convert FILE CSS content into a list representation.
See `org-msg-css-to-list'."
(with-temp-buffer
(insert-file-contents file)
(org-msg-css-to-list)))
(defun org-msg-props-to-style (props)
"Convert PROPS properties to a CSS style string."
(cl-flet ((css-str (css)
(concat (symbol-name (car css)) ":"
(cdr css) ";")))
(apply 'concat (mapcar #'css-str props))))
(defsubst org-msg-in-quote-block ()
"Whether point is in a quote block."
(let ((face (get-char-property (point) 'face)))
(if (listp face)
(cl-find 'org-quote face)
(eq 'org-quote face))))
(defun org-msg-ascii-blockquote (level begin end)
"Recursively convert lines matching the `^ ?>+ ' regular
expression into multi-level quote blocks."
(let ((suffix (format "quote%d\n" level)))
(goto-char begin)
(while (re-search-forward "^ ?>+ " end t)
(if (and (= level 0) (org-msg-in-quote-block))
(org-msg-ascii-blockquote (1+ level) begin end)
(unless (org-in-src-block-p)
(goto-char (line-beginning-position))
(let ((new-begin (point-marker)))
(insert "#+begin_" suffix)
(if (re-search-forward "^\\([^ >]\\| [^>]\\)" end t)
(goto-char (line-beginning-position))
(goto-char (point-max))
(unless (= (point) (line-beginning-position))
(insert "\n")))
(insert "#+end_" suffix)
(let ((new-end (point-marker)))
(goto-char new-begin)
(while (re-search-forward "^ ?>" new-end t)
(replace-match "")
(forward-char 1))
(org-msg-ascii-blockquote (1+ level) new-begin new-end))))))))
(defun org-msg-build-style (tag class css)
"Given a TAG and CLASS selector, it builds a CSS style string.
This string can be used as a HTML style attribute value."
(cl-flet ((css-match-p (css)
(or (and (eq tag (car css))
(eq class (cadr css)))
(and (not (car css))
(eq class (cadr css)))
(and (not (cadr css))
(eq tag (car css))))))
(when-let ((sel (cl-remove-if-not #'css-match-p css))
(props (apply 'append (mapcar 'caddr sel))))
(org-msg-props-to-style props))))
(defun org-msg-str-to-mailto (str css)
"Convert a string of mail addresses into mailto anchor links.
Takes a string STR as a parameter and build a list of string and
mailto anchor link. If a CSS style list is provided and a 'a
selectors on class `org-msg-reply-header-class', it sets the
style mailto anchor link style appropriately."
(with-temp-buffer
(insert str)
(let ((name-regexp "\\([[:alpha:]\"][[:alnum:] ,\"()@./-]+\\)")
(mail-regexp "<\\([A-Za-z0-9@.-]+\\)>")
(cursor (goto-char (point-min)))
(style (org-msg-build-style 'a org-msg-reply-header-class css))
(res))
(while (re-search-forward (concat name-regexp " " mail-regexp) nil t)
(unless (= (match-beginning 0) cursor)
(push (buffer-substring cursor (match-beginning 0))
res)
(setq cursor (match-end 0)))
(let ((anchor `(a ((href . ,(concat "mailto:" (match-string 0))))
,(delete ?\" (match-string 1)))))
(when style
(push `(style . ,style) (cadr anchor)))
(push anchor res)))
(nreverse res))))
(defmacro org-msg-list-foreach (spec &rest body)
"Loop over a list.
Evaluate BODY with VAR bound to each cons from LIST, in turn.
Then evaluate RESULT to get return value, default nil.
\(fn (VAR LIST) BODY...)"
(declare (indent 1))
`(let ((,(car spec) ,(cadr spec)))
(while ,(car spec)
,@body
(let ((temp ,(car spec)))
(setq ,(car spec) (cdr temp))))))
(defun org-msg-improve-reply-header (xml css)
"Aesthetically improve the reply header.
The reply header (From, Subject, Date, ...) generated by
`gnus-article-browse-html-article' does not look very nice. XML
is the XML tree and CSS the style."
(let ((div (assq 'div (assq 'body xml))))
;; Delete unnecessary line break
(let ((e (cdr div)))
(while e
(if (and (stringp (car e))
(eq (cl-caadr e) 'br)
(and (stringp (caddr e))
(string-prefix-p "\n " (caddr e))))
(progn
(setcar e (replace-regexp-in-string "\n +" " "
(concat (car e) (cl-caddr e))))
(setcdr e (cl-cdddr e)))
(setf e (cdr e)))))
;; Add a bold property to the prefixes like "From", "Date", "Subject",
;; ... This section also deletes the undesirable header lines as
;; specified by `org-msg-undesirable-headers'.
(let ((e (cdr div)))
(while e
(if (stringp (cadr e))
(let ((prefix (car (split-string (cadr e) ":"))))
(if (cl-find prefix org-msg-undesirable-headers
:test (lambda (x y) (string-match-p y (string-trim x))))
(setcdr e (cdddr e))
(setcar (cdr e) (replace-regexp-in-string prefix "" (cadr e)))
(setcdr e (cons `(b nil ,(capitalize prefix)) (cdr e)))
(setf e (cddr e))))
(setf e (cdr e)))))
;; Transform mail addresses into "mailto" links
(org-msg-list-foreach (e (cdr div))
(when (stringp (cadr e))
(when-let ((mailto (org-msg-str-to-mailto (cadr e) css)))
(setf mailto (append mailto (cddr e)))
(setcdr e mailto))))
(when css
(assq-delete-all 'hr (assq 'body xml))
(assq-delete-all 'align (cadr div))
(setf (cadr div) (assq-delete-all 'style (cadr div)))
(let ((div-style (org-msg-build-style 'div
org-msg-reply-header-class css))
(p-style (org-msg-build-style 'p org-msg-reply-header-class css)))
(when div-style
(push `(style . ,div-style) (cadr div)))
(when p-style
(setf (cddr div) `((p ((style . ,p-style)) ,@(cddr div)))))))))
(defun org-msg-xml-walk (xml fun)
"Recursively walk a XML tree and call FUN on each node."
(when (listp xml)
(funcall fun xml)
(dolist (e (cddr xml))
(org-msg-xml-walk e fun))))
(defun org-msg-html-buffer-to-xml (&optional base)
"Return the XML tree of the current HTML buffer.
BASE is the path used to convert the IMG SRC relative paths to
absolute paths. Base is also used to locate SVG objects tag file
and include the SVG content into the email XML tree."
(let ((dirs (list base (temporary-file-directory))))
(cl-flet* ((get-html-root (xml)
(catch 'found
(org-msg-xml-walk xml (lambda (x)
(when (eq (car x) 'html)
(throw 'found x))))))
(get-file-path (file)
(let ((paths (cl-mapcar (lambda (d)
(expand-file-name file d))
dirs)))
(car (cl-delete-if-not 'file-exists-p paths))))
(make-img-abs (xml)
(when (eq (car xml) 'img)
(when-let ((src (assq 'src (cadr xml)))
(file (cdr src)))
(unless (or (url-type (url-generic-parse-url file))
(file-name-absolute-p file))
(if-let ((path (get-file-path file)))
(setcdr src path)
(unless (y-or-n-p (format "'%s' Image is missing,\
do you want to continue ?" file))
(error "'%s' Image is missing" file)))))))
(inline-svg (xml)
(when (and (eq (car xml) 'object)
(string= (cdr (assq 'type (cadr xml)))
"image/svg+xml"))
(let ((file (get-file-path (assoc-default 'data (cadr xml)))))
(when file
(let ((svg (with-temp-buffer
(insert-file-contents file)
(when (search-forward "<svg " nil t)
(libxml-parse-xml-region (match-beginning 0)
(point-max))))))
(setcar xml (car svg))
(setcdr xml (cdr svg))))))))
(let ((xml (libxml-parse-html-region (point-min) (point-max))))
(setf xml (get-html-root xml))
(when base
(org-msg-xml-walk xml #'make-img-abs)
(org-msg-xml-walk xml #'inline-svg))
(assq-delete-all 'title (assq 'head xml))
xml))))
(defun org-msg-load-html-file (file)
"Return the XML tree of a HTML FILE."
(with-temp-buffer
(insert-file-contents file)
(org-msg-html-buffer-to-xml (file-name-directory file))))
(defun org-msg--html-quote-block (quote-block contents info)
(let ((cur (car (org-element-property :attr_html quote-block))))
(unless (and cur (string-match-p ":class " cur))
(let ((attr (concat ":class quote0" (when cur " ") cur)))
(org-element-put-property quote-block :attr_html (list attr)))))
(org-html-quote-block quote-block contents info))
(defun org-msg--html-special-block (special-block contents info)
"Similar to `org-html-special-block' but treat specially the
blocks of type \"quote...\" generated by `org-msg-ascii-blockquote'."
(let ((block-type (org-element-property :type special-block)))
(cond
((string-match "quote[0-9]+" block-type)
(let* ((contents (or contents ""))
(a (org-html--make-attribute-string `(:class ,block-type))))
(format "<blockquote %s>\n%s\n</blockquote>" a contents)))
(t (org-html-special-block special-block contents info)))))
(defun org-msg-org-to-xml (str &optional base)
"Transform the STR Org string into a XML tree.
BASE is the path used to convert the IMG SRC relative paths to
absolute paths."
(save-window-excursion
(with-temp-buffer
(insert str)
(when org-msg-convert-citation
(org-msg-ascii-blockquote 0 (point-min-marker) (point-max-marker)))
(let ((org-html-table-default-attributes nil)
(org-html-htmlize-output-type 'inline-css)
(org-html-head-include-scripts nil)
(org-html-head-include-default-style nil)
(org-msg-export-in-progress t))
(let ((buf (generate-new-buffer-name " *OrgMsg HTML Export*")))
(with-current-buffer (org-export-to-buffer 'org-msg-html buf)
(let ((xml (org-msg-html-buffer-to-xml base)))
(kill-buffer)
xml)))))))
(defun org-msg-export-as-text (charset str)
"Transform the Org STR into a plain text."
(with-temp-buffer
(insert str)
(cl-letf (((symbol-function #'fill-region) #'ignore))
(let ((org-ascii-charset charset)
(org-ascii-inner-margin 0)
(files '()))
(with-current-buffer (org-ascii-export-as-ascii)
(while (re-search-forward "<file:\\\([a-z0-9AZ_\./-]+\\\)>" nil t)
(setf files (push (match-string-no-properties 1) files)))
(cl-values (buffer-string) files))))))
(defun org-msg-export-as-html (str)
"Transform the Org STR into html."
(prog2
(org-export-define-derived-backend 'org-msg-html 'html
:translate-alist `((special-block . org-msg--html-special-block)
(quote-block . org-msg--html-quote-block)
,@(org-export-get-all-transcoders 'html)))
(org-msg-xml-to-str (org-msg-build str))
(setq org-export-registered-backends
(cl-delete-if (apply-partially 'eq 'org-msg-html)
org-export-registered-backends
:key 'org-export-backend-name))))
(defun org-msg-load-css ()
"Load the CSS definition according to `org-msg-enforce-css'."
(cond ((listp org-msg-enforce-css) org-msg-enforce-css)
((stringp org-msg-enforce-css)
(org-msg-css-file-to-list org-msg-enforce-css))))
(defmacro org-msg-with-match-prop (prop &rest body)
"Look for the Org PROP property and call @BODY on match."
(declare (indent 1))
`(save-excursion
(goto-char (point-min))
(when (re-search-forward (org-re-property ,prop nil t) nil t)
(progn ,@body))))
(defun org-msg-get-prop (prop)
"Return the Org PROP property value, nil if undefined."
(org-msg-with-match-prop prop
(read (match-string-no-properties 3))))
(defun org-msg-set-prop (prop val)
"Set the Org PROP property value to VAL."
(org-msg-with-match-prop prop
(replace-match (format "%S" val) nil nil nil 3)))
(defun org-msg-build (org)
"Build and return the XML tree for ORG string."
(let ((css (org-msg-load-css)))
(cl-flet ((enforce (xml)
(let* ((tag (car xml))
(tmp (assq 'class (cadr xml)))
(class (when tmp
(intern (cdr tmp))))
(style (org-msg-build-style tag class css)))
(when style
(setf (cadr xml) (assq-delete-all 'style (cadr xml)))
(setf (cadr xml) (assq-delete-all 'class (cadr xml)))
(push `(style . ,style) (cadr xml)))))
(fix-img-src (xml)
(let ((src (assq 'src (cadr xml))))
(when (string-prefix-p "file://" (cdr src))
(setcdr src (substring (cdr src) (length "file://"))))))
(set-charset (xml)
(when (eq 'meta (car xml))
(let ((l (cadr xml)))
(cond ((string= (downcase (alist-get 'http-equiv l "?"))
"content-type")
(setf (alist-get 'content l)
(format "text/html;charset=%s"
org-html-coding-system)))
((alist-get 'charset l)
(setf (alist-get 'charset l)
(symbol-name org-html-coding-system))))))))
(let* ((reply (org-msg-org-to-xml org default-directory))
(temp-files (org-msg-get-prop "reply-to"))
(original (when temp-files
(org-msg-load-html-file (car temp-files)))))
(assq-delete-all 'h1 (assq 'div (assq 'body reply)))
(org-msg-xml-walk (assq 'body reply) #'fix-img-src)
(when css
(assq-delete-all 'style (assq 'head reply))
(org-msg-xml-walk (assq 'body reply) #'enforce))
(if (not original)
(assq-delete-all 'script (assq 'head reply))
(org-msg-improve-reply-header original css)
(push (or (assq 'article (assq 'body reply))
(assq 'div (assq 'body reply)))
(cddr (assq 'body original))))
(when original
(org-msg-xml-walk original #'set-charset))
(or original reply)))))
(defun org-msg-preview (arg)
"Export and display the current OrgMsg buffer.
It uses the last alternative of the `alternatives' property as
the alternatives should be listed in increasing order of
preference. If this alternative is `html' it calls the
`browse-url' function to display the exported mail in a web
browser. With the prefix argument ARG set, it calls
`xwidget-webkit-browse-url' instead of `browse-url'. For all
other alternatives, it displays the exported result in a buffer."
(interactive "P")
(let* ((preferred (last (org-msg-get-prop "alternatives")))
(alt (caar (org-msg-build-alternatives preferred t))))
(cond ((string= (car alt) "text/html")
(save-window-excursion
(let ((browse-url-browser-function (if arg
'xwidget-webkit-browse-url
browse-url-browser-function))
(tmp-file (make-temp-file "org-msg" nil ".html")))
(with-temp-buffer
(insert (cdr alt))
(write-file tmp-file))
(browse-url (concat "file://" tmp-file)))))
(t (with-current-buffer (get-buffer-create
(format "*OrgMsg %s Preview*" (car alt)))
(delete-region (point-min) (point-max))
(insert (cdr alt)))
(display-buffer (current-buffer))))))
(defun org-msg-separate-mml-and-org (&optional preserve)
"Separate the Org Mode and the MML content of the current buffer.
Returns the MML content and the Org Mode content as a list of two
strings. If PRESERVE is nil, the MML content is removed from the
buffer otherwise, the buffer is left untouched."
(let ((buf (current-buffer))
mml org)
(when preserve
(setf buf (generate-new-buffer " *temp*"))
(copy-to-buffer buf (point-min) (org-msg-end)))
(with-current-buffer buf
(goto-char (point-min))
(let (stack res)
(while (re-search-forward "<#\\\(/?[a-z]+\\\)[ >]" nil t)
(unless (org-in-block-p '(""))
(let ((tag (match-string-no-properties 1)))
(unless (string= tag "secure")
(if (string= (substring tag 0 1) "/")
(let ((cur (pop stack)))
(while (not (string= (substring tag 1) (car cur)))
(setf cur (pop stack)))
(unless stack
(push (list (cdr cur) (line-end-position)) res)))
(push (cons tag (line-beginning-position)) stack))))))
(setf mml (mapconcat (lambda (x)
(apply 'delete-and-extract-region x))
res "\n"))
(setf org (buffer-substring (org-msg-start) (org-msg-end))))
(when preserve
(kill-buffer)))
(cl-values mml org)))
(defun org-msg-build-alternatives (alternatives &optional preserve)
"Build the contents of the current Org-msg buffer for each of the ALTERNATIVES.
If PRESERVE is t, it does not alter the content of the
buffer (cf. `org-msg-separate-mml-and-org').
Returns a list of three items:
1. An association list of the exported alternatives
2. A list of attachments generated during the exportation if any
3. MML tags as a string if any"
(let (mml org files)
(cl-multiple-value-setq (mml org)
(org-msg-separate-mml-and-org preserve))
(cl-flet ((export (alt)
(let ((exporter (cdr (assq alt org-msg-alternative-exporters))))
(unless exporter
(error "%s is not a valid alternative, must be one of %s"
alt (mapcar #'car org-msg-alternative-exporters)))
(let ((exported (funcall (cdr exporter) org))
(exp-files '()))
(when (listp exported)
(cl-multiple-value-setq (exported exp-files) exported))
(setf files (append files exp-files))
(cons (car exporter) exported)))))
(cl-values (mapcar #'export alternatives) files mml))))
(defun org-msg-prepare-to-send ()
"Convert the current OrgMsg buffer into `mml' content.
This function is a hook for `message-send-hook'."
(save-window-excursion
(when (eq major-mode 'org-msg-edit-mode)
(if (get-text-property (org-msg-start) 'mml)
(message "Warning: org-msg: %S is already a MML buffer" (current-buffer))
(let ((alternatives (org-msg-get-prop "alternatives"))
attachments mml)
(cl-multiple-value-setq (org-msg-alternatives attachments mml)
(org-msg-build-alternatives alternatives))
(when (memq 'html alternatives)
(cl-flet ((is-image-but-svg (file)
(string-match-p "image/\\([^s]\\|s[^v]\\|sv[^g]\\)"
(org-msg-file-mime-type file))))
(setf attachments (cl-delete-if #'is-image-but-svg attachments))))
(setf attachments (cl-union (org-msg-get-prop "attachment")
attachments))
;; Verify all attachments exist
(dolist (file attachments)
(unless (file-exists-p file)
(error "File '%s' does not exist" file)))
;; Clear the contents of the message
(goto-char (org-msg-start))
(delete-region (org-msg-start) (point-max))
;; If mml has recursive html support (starting with Emacs 28), we want
;; to generate the structure of the MIME document here. If not we do
;; this by manually editing the structure of the parsed MML tree in
;; `org-msg-mml-into-multipart-related'. We also don't need to worry
;; about this if we are only sending text/plain
(if (or (org-msg-mml-recursive-support)
(not (memq 'html alternatives)))
(progn
(when (or attachments mml)
(mml-insert-multipart "mixed"))
(when (> (length org-msg-alternatives) 1)
(mml-insert-multipart "alternative"))
(dolist (alt org-msg-alternatives)
(mml-insert-part (car alt))
(insert (cdr alt))
(forward-line))
(when (> (length org-msg-alternatives) 1)
(forward-line))
(dolist (file attachments)
(mml-insert-tag 'part 'type (org-msg-file-mime-type file)
'filename file 'disposition "attachment"))
(when mml
(insert mml)))
(mml-insert-part "text/html")
(insert (cdr (assoc "text/html" org-msg-alternatives)))
;; Pass data to `org-msg-mml-into-multipart-related'
(setq org-msg-attachment attachments
org-msg-mml mml))
;; Propertise the message contents so we don't accidentally run this
;; function on the buffer twice
(add-text-properties (save-excursion (message-goto-body))
(point-max)
'(mml t)))))))
(defun org-msg-file-mime-type (file)
"Return FILE mime type based on FILE extension.
If FILE does not have an extension, \"text/plain\" is returned."
(if-let ((extension (file-name-extension file)))
(mailcap-extension-to-mime extension)
"text/plain"))
(defun org-msg-mml-into-multipart-related (orig-fun cont)
"Extend the capability to handle file attachments.
This function is used as an advice function of
`mml-expand-html-into-multipart-related'.
- ORIG-FUN is the original function.
- CONT is the MIME representation of the mail content.
The implementation depends on the `org-msg-attachment' temporary