1 # -*- coding: utf-8 -*-
2 # Copyright Neil Brown ©2021-2023 <neil@brown.name>
3 # May be distributed under terms of GPLv2 - see file:COPYING
5 # edlib module for composing email.
6 # Message is composed in a text document with some markers that
7 # cannot be edited that contain extra information. These include
8 # - end-of-headers marker
10 # - sign/encrypt request (maybe later)
13 # Markers are a single line, are drawn by a special handler, and
14 # can only be editted by popping up a dialog pane. They have a vmark
15 # at each end with attributes attached to the first. The text of the line
16 # starts "@#!compose" so it can be re-marked when a view is opened.
18 # All text is utf-8, and is encoded if necessary when the message is
19 # sent. "sending" causes the whole message to be encoded into a temporary
20 # file, which is then given to a configured program (e.g. /usr/sbin/sendmail)
22 # Text is encoded as quoted-printable if needed, and attachments are
23 # combined in a multipart/mixed.
26 # - End of headers is marked with "@#!compose:headers\n"
27 # - An attachedment is marked with
28 # @#!compose:attach type= filename= disposition= description=
29 # The content of filename is url encoded
40 import email.headerregistry
45 from datetime import date
47 def read_status(p, key, focus, **a):
48 out, err = p.communicate()
49 focus.call("Message", "Email submission complete")
50 edlib.LOG("Email submission reported: " +
51 out.decode('utf-8','ignore') + err.decode('utf-8','ignore'))
54 class compose_email(edlib.Pane):
55 def __init__(self, focus):
56 edlib.Pane.__init__(self, focus)
57 self.view = focus.call("doc:add-view", self) - 1
58 self.complete_start = None
59 self.complete_menu = None
62 m, l = self.vmarks(self.view)
64 self.insert_header_mark()
66 m = focus.call("doc:point", ret='mark')
68 # At start of file - find a better place.
69 # If there is an empty header, go there, else after headers.
70 if not self.find_empty_header(m):
73 # track point movement so it can be moved out of a marker
74 self.point = focus.call("doc:point", ret='mark')
75 self.prev_point = None
76 self.have_prev = False
77 self.call("doc:request:mark:moving")
79 def add_headers_new(self, key, focus, **a):
80 "handle:compose-email:empty-headers"
81 fr = focus['email:from']
83 nm = focus['email:name']
85 fr = "\"%s\" <%s>" %(nm, fr)
87 self.check_header("From", fr)
88 self.check_header("To")
89 self.check_header("Cc")
90 self.check_header("Subject")
91 # mark the document as unmodified
92 self.call("doc:modified", -1)
94 self.find_empty_header(m)
95 self.call("Move-to", m)
98 def copy_headers(self, key, focus, str, **a):
99 "handle:compose-email:copy-headers"
101 # get date - it might be useful
102 focus.call("doc:multipart-0-list-headers", "date", self.copy_date)
103 # need to collect addresses even if I don't use them
104 # so that I can pick the right "from" address
106 focus.call("doc:multipart-0-list-headers", "to", self.copy_addrs)
107 to_addrs = self.filter_cc(self.addrlist)
109 focus.call("doc:multipart-0-list-headers", "cc", self.copy_addrs)
110 cc_addrs = self.filter_cc(self.addrlist)
112 focus.call("doc:multipart-0-list-headers", "reply-to", self.copy_addrs)
113 if not self.addrlist:
114 focus.call("doc:multipart-0-list-headers", "from", self.copy_addrs)
115 from_addrs = self.filter_cc(self.addrlist)
116 if str != "reply-all":
121 me = self['email:from']
123 nm = self['email:name']
125 me = "\"%s\" <%s>" %(nm, me)
126 self.check_header("From", me)
128 n,a = self.addrlist[0]
129 self['reply-author'] = n if n else a
133 self.add_addr_header("To", from_addrs)
138 self.add_addr_header("Cc", to_addrs)
140 self.add_addr_header("To", to_addrs)
141 self.add_addr_header("Cc", cc_addrs)
146 self.check_header("To")
147 self.check_header("Cc")
148 focus.call("doc:multipart-0-list-headers", "subject", self.copy_subject)
151 focus.call("doc:multipart-0-list-headers", "references", self.copy_addrs)
152 if not self.addrlist:
153 focus.call("doc:multipart-0-list-headers", "in-reply-to", self.copy_addrs)
154 l = len(self.addrlist)
155 focus.call("doc:multipart-0-list-headers", "message-id", self.copy_addrs)
156 if l < len(self.addrlist):
157 self.add_addr_header("In-reply-to", self.addrlist[l:], True)
158 self.add_addr_header("References", self.addrlist, True)
159 # mark the document as unmodified
160 self.call("doc:modified", -1)
163 self.find_empty_header(m)
164 self.call("Move-to", m)
167 def copy_date(self, key, focus, str, **a):
168 self['date-str'] = str
169 d = email.utils.parsedate_tz(str)
171 self['date-seconds'] = "%d" % email.utils.mktime_tz(d)
174 def copy_addrs(self, key, focus, str, **a):
175 addr = email.utils.getaddresses([str])
176 self.addrlist.extend(addr)
179 def copy_subject(self, key, focus, str, **a):
180 m2, l = self.vmarks(self.view)
183 if not subj.lower().startswith(self.pfx.lower() + ':'):
184 subj = self.pfx + ': ' + subj
185 self.call("doc:replace", m2, m2, ("Subject: "+ subj + "\n"))
188 def from_lists(self):
190 f = self['email:from']
193 af = self['email:altfrom']
195 for a in af.strip().split("\n"):
200 af = self['email:deprecated_from']
202 for a in af.strip().split("\n"):
205 dme.append(a.strip())
208 def filter_cc(self, list):
209 # every unique address in list that isn't my address gets added
213 me, dme = self.from_lists()
214 for name,addr in list:
218 elif addr not in addrs and addr not in dme:
220 ret.append((name, addr))
223 def add_addr_header(self, hdr, addr, need_angle = False):
226 m2, l = self.vmarks(self.view)
231 # Note that we must call doc:replace on parent else
232 # we might be caught trying to insert a non-newline immediately
233 # before a marker. We do eventuall insert a newline, so it is safe.
236 na = "\"%s\" <%s>" %(n, a)
242 self.parent.call("doc:replace", m2, m2, prefix,
243 ",render:rfc822header-wrap=%d"%len(prefix))
245 self.parent.call("doc:replace", m2, m2, prefix)
248 self.parent.call("doc:replace", m2, m2, na)
250 # we wrote a header, so write a newline
251 self.parent.call("doc:replace", m2, m2, "\n")
253 def find_empty_header(self, m):
254 m2,l = self.vmarks(self.view)
256 self.call("text-search", "^[!-9;-~]+\\h*:\\h*$", m, m2)
260 def find_any_header(self, m):
261 m2, l = self.vmarks(self.view)
263 self.call("text-search", "^[!-9;-~]+\\h*:\\h*", m, m2)
267 def to_body(self, m):
268 m2, l = self.vmarks(self.view)
274 def handle_quote_content(self, key, focus, str, **a):
275 "handle:compose-email:quote-content"
278 who = self['reply-author']
281 n = self['date-seconds']
283 d = date.fromtimestamp(int(n))
284 when = d.strftime("%a, %d %b %Y")
286 when = 'a recent day'
287 q = "On %s, %s wrote:\n" % (when, who)
288 for l in str.split("\n"):
289 q += '> ' + l.strip('\r') + '\n'
290 self.call("doc:replace", m, m, q)
291 # mark the document as unmodified
292 self.call("doc:modified", -1)
295 def find_markers(self):
299 r = self.parent.call("text-search", "^@#!compose:", m)
300 except edlib.commandfailed:
305 self.parent.call("doc:EOL", -1, m)
306 m1 = edlib.Mark(self, self.view)
308 self.parent.call("doc:set-attr", m,
309 "markup:func", "compose:markup-header")
310 m2 = edlib.Mark(orig=m1)
311 self.parent.call("doc:EOL", 1, m2, 1)
312 s = self.parent.call("doc:get-str", m1, m2, ret='str')
314 m1['compose-type'] = s.split(' ')[0]
317 def insert_header_mark(self):
318 # insert header at first blank line, or end of file
319 m = edlib.Mark(self, self.view)
321 self.call("text-search", "^$", m)
323 self.call("doc:set-ref", 0, m)
324 m2 = edlib.Mark(orig=m)
326 self.parent.call("doc:replace", m2, m, "@#!compose:headers\n",
327 "/markup:func=compose:markup-header/")
328 m2['compose-type'] = 'headers'
330 def check_header(self, header, content = ""):
331 # if header doesn't already exist, add it at end
332 m1 = edlib.Mark(self)
333 m2, l = self.vmarks(self.view)
335 self.call("text-search", "^(?i:%s)\\h*:" % header, m1, m2)
337 self.call("doc:replace", m2, m2, header + ': ' + content+'\n')
339 def markup_header(self, key, focus, num, mark, mark2, comm2, **a):
340 "handle:compose:markup-header"
341 # at least go to end of line
342 self.parent.call("doc:EOL", 1, mark)
343 m = self.vmark_at_or_before(self.view, mark)
346 type = m['compose-type']
347 if type == "headers":
348 markup = "<fg:red>Headers above, content below"
350 markup = "<fg:cyan-40>[section: %s]" % type
351 info = m['compose-info']
353 markup += ' - ' + info
355 # normal render - go past eol
356 self.parent.next(mark)
358 # return num==0 to display cursor at start or end, anything else
359 # should be suppressed.
360 return comm2("cb", focus, 0, markup)
362 def handle_clone(self, key, focus, **a):
364 p = compose_email(focus)
365 self.clone_children(p)
368 def map_attr(self, key, focus, str, str2, mark, comm2, **a):
370 if not str or not mark or not comm2:
373 if str == "render:compose-email-menu":
374 comm2("cb", focus, mark, "menu_here", 1, 250)
377 if str == "render:rfc822header-wrap":
378 comm2("attr:callback", focus, int(str2), mark, "wrap", 30)
381 # get previous mark and see if it is here
382 m = self.vmark_at_or_before(self.view, mark)
383 if not m and str == "start-of-line":
384 # start of a header line - set colour for tag and header, and wrap info
385 rv = self.call("text-match", "(\\h+|[!-9;-~]+\\h*:)", mark.dup())
387 # make space or tag light blue, and body dark blue
388 # If tag is unknown, make it grey
389 rv2 = self.call("text-match",
390 "?i:(from|to|cc|subject|in-reply-to|references|date|message-id):",
393 comm2("cb", focus, mark, rv-1, "fg:blue+30", 20)
395 comm2("cb", focus, mark, rv-1, "fg:black+30", 20)
396 comm2("cb", focus, mark, 0, "fg:blue-70,bold", 10)
398 # make whole line red
399 comm2("cb", focus, mark, 0, "bg:red+50,fg:red-50", 20)
400 comm2("cb", focus, mark, 0, "wrap-tail: ,wrap-head: ", 1)
401 return edlib.Efallthrough
402 if m and str == 'start-of-line':
403 # if line starts '>', give it some colour
404 if focus.following(mark) == '>':
405 colours = ['red', 'red-60', 'green-60', 'magenta-60']
409 while c and c in ' >':
414 if cnt >= len(colours):
416 comm2("cb", focus, mark, 0, "fg:"+colours[cnt-1], 20)
417 return edlib.Efallthrough
419 return edlib.Efallthrough
421 def handle_replace(self, key, focus, mark, mark2, str, **a):
423 self.complete_start = None
424 self.complete_end = None
425 if self.complete_menu:
426 self.complete_menu("Window:Close")
427 self.complete_menu = None
429 mark = focus.call("doc:point", ret='mark')
432 self.parent.call("doc:set-ref", mark2, 0)
433 if not mark or not mark2:
437 mark, mark2 = mark2, mark
439 m = self.vmark_at_or_before(self.view, mark)
440 if m and m['compose-type']:
441 # must not edit a marker, but can insert a "\n" before
442 if mark == mark2 and mark == m and str and str[-1] == '\n':
443 # inserting newline at start
445 return edlib.Efallthrough
448 # not a delete, so should be safe as long as marks aren't before m
451 return edlib.Efallthrough
452 m2 = self.vmark_at_or_before(self.view, mark2)
453 if m2 and m2.prev() == m and mark2 == m2:
454 # deleting text just before a marker is OK as long as we will have
455 # a newline before the marker
456 if (str and str[-1] == '\n') or focus.prior(mark) == '\n':
457 return edlib.Efallthrough
459 # not completely within a part, so fail
461 # deleting/replacing something after m/m2.
464 return edlib.Efallthrough
466 def handle_moving(self, key, focus, mark, mark2, **a):
468 if mark == self.point and not self.have_prev:
469 # We cannot dup because that triggers a recursive notification
470 #self.prev_point = mark.dup()
471 self.prev_point = self.vmark_at_or_before(self.view, mark)
472 self.have_prev = True
473 self.damaged(edlib.DAMAGED_VIEW)
476 def handle_review(self, key, focus, **a):
477 "handle:Refresh:view"
478 # if point is in a "header" move it to start or end
479 # opposite prev_point
480 if not self.have_prev:
482 m = self.vmark_at_or_before(self.view, self.point)
483 if m and m != self.point and m['compose-type']:
484 if not self.prev_point or self.prev_point < self.point:
485 # moving toward end of file
488 self.point.to_mark(m)
489 self.prev_point = None
490 self.have_prev = False
493 def handle_doc_get_attr(self, key, focus, mark, str, comm2, **a):
494 "handle:doc:get-attr"
495 if not mark or not str or not comm2 or not str.startswith("fill:"):
496 return edlib.Efallthrough
497 if str == "fill:repeating-prefix":
498 comm2("cb", focus, mark, ">", str)
500 m = self.vmark_at_or_before(self.view, mark)
502 return edlib.Efallthrough
503 # in headers, need a min-prefix and start-re for fill
504 if str == "fill:default-prefix":
505 comm2("cb", focus, mark, " ", str)
507 if str == "fill:start-re" or str == "fill:end-re" :
508 comm2("cb", focus, mark, "^($|[^\\s])", str)
511 def try_address_complete(self, m):
512 this = self.this_header(m.dup())
513 if not this or this not in ["to","cc"]:
516 word = self.prev_addr(st)
519 if ('@' in word and '.' in word and
520 ('>' in word or '<' not in word)):
521 # looks convincing - nothing to complete here
524 # some other completion pending.
529 self.call("Message", "Trying to complete %s ..." % word)
530 self.complete_word = word
531 self.p = subprocess.Popen(["notmuch-addr", word],
532 stdout=subprocess.PIPE,
533 stderr=subprocess.PIPE)
534 self.complete_end = m.dup()
535 self.call("event:read", self.p.stdout.fileno(), self.get_complete)
538 def get_complete(self, key, focus, **a):
541 out,err = self.p.communicate()
545 self.call("Message", "Address expansion gave error: %s" %
546 err.decode('utf-8','ignore'))
549 "No completions found for address %s" % self.complete_word)
551 pt = focus.call("doc:point", ret='mark')
552 if not pt or not self.complete_end or pt != self.complete_end:
553 # point moved, do nothing
554 self.complete_end = None
556 self.complete_start = None
557 self.complete_end = None
558 if self.complete_menu:
559 self.complete_menu.call("Window:Close")
560 self.complete_menu = None
562 word = self.prev_addr(st)
565 complete_list = out.decode('utf-8','ignore').strip().split("\n")
566 if len(complete_list) > 1:
567 self.call("Message", "%d completions found for address %s"
568 % (len(complete_list), word))
569 self.complete_start = st
570 st["render:compose-email-menu"] = "here"
571 mp = self.call("attach-menu", ret='pane')
572 for c in complete_list:
573 mp.call("menu-add", c)
574 mp.call("doc:file", -1)
575 self.complete_menu = mp
576 self.add_notify(mp, "Notify:Close")
577 self.complete_end = pt.dup()
580 self.call("Message", "only 1 completion found for address %s"
582 self.parent.call("doc:replace", st, pt, complete_list[0])
585 def handle_close(self, key, focus, **a):
586 "handle:Notify:Close"
587 if focus == self.complete_menu:
588 self.complete_menu = None
591 def menu_done(self, key, focus, str1, **a):
593 if self.complete_start and str1:
594 self.call("doc:replace", str1, self.complete_end, self.complete_start)
595 self.complete_start = None
596 self.complete_end = None
597 self.complete_menu = None
600 def handle_draw(self, key, focus, str2, xy, **a):
602 if not self.complete_menu or not str2 or ",menu_here" not in str2:
603 return edlib.Efallthrough
605 p = self.complete_menu.call("ThisPopup", ret='pane')
607 xy = p.parent.mapxy(focus, xy[0], focus.h)
610 if p.h > p.parent.h - p.y:
611 # FIXME how do I avoid the border provided by lib-view
612 p.h = p.parent.h - p.y
613 return edlib.Efallthrough
615 def try_cycle_from(self, m):
618 this = self.this_header(start, end)
619 if not this or this != "from":
621 current = self.call("doc:get-str", start, end, ret='str')
622 name, addr = email.utils.parseaddr(current)
623 me, dme = self.from_lists()
625 self.call("Message", "No From addresses declared")
628 i = me.index(addr) + 1
631 if i < 0 or i >= len(me):
635 self.call("doc:replace", start, end,
636 "%s <%s>" % (name, addr))
640 def this_header(self, mark, end = None, downcase = True):
642 l = self.call("text-search", "^[!-9;-~]+\\h*:", 0, 1, mark)
647 s = self.call("doc:get-str", m1, mark, ret='str')
649 while self.following(mark) == ' ':
652 ret = s.strip().lower()
655 except edlib.commandfailed:
659 # Now find the body - mark is at the start
661 mbody, l = self.vmarks(self.view)
663 self.call("text-search", "^[!-9;-~]", end, mbody)
666 self.call("doc:EOL", -1, 1, end)
669 def prev_addr(self, m):
670 # if previous char is not a space, collect everything
671 # back to , or : or \n and return 1
673 if not c or c in " \n":
676 while c and c not in ",:\n":
679 while c and c in ",: \n":
685 def handle_tab(self, key, focus, mark, **a):
687 m2, l = self.vmarks(self.view)
689 return edlib.Efallthrough
690 # in headers, TAB does various things:
691 # In 'to' or 'cc' if the preceeding word looks like an
692 # incomplete address, then address completion is tried.
693 # Otherwise goes to next header if there is one, else to
695 if (not self.try_address_complete(mark) and
696 not self.try_cycle_from(mark) and
697 not self.find_any_header(mark)):
700 def handle_s_tab(self, key, focus, mark, **a):
702 m2, l = self.vmarks(self.view)
704 # After header, S:Tab goes to last header
707 while self.find_any_header(m):
710 # in headers, go to previous header
713 while self.find_any_header(m) and m < m2:
717 def handle_attach(self, key, focus, **a):
719 p = focus.call("PopupTile", "2", "", ret='pane')
722 p['prompt'] = "Attachment"
723 p['done-key'] = "compose-email:attach"
724 p.call('doc:set-name', "Attachment File")
725 p['pane-title'] = "Attachment File"
726 p = p.call("attach-history", "*Attachment History*",
728 p.call("attach-file-entry", "file")
731 def handle_do_attach(self, key, focus, str1, str2, **a):
732 "handle:compose-email:attach"
738 (type, encoding) = mimetypes.guess_type(str1, False)
740 type = "application/octet-stream"
741 f, l = self.vmarks(self.view)
742 m = edlib.Mark(orig=l)
743 self.call("doc:set-ref", 0, m)
744 m2 = edlib.Mark(orig=m)
745 # Make sure these 2 are the very last marks.
748 self.parent.call("doc:replace", m2, m,
749 "@#!compose:attach filename=%s type=%s\n" %
750 (urllib.parse.quote(str1), type),
751 "/markup:func=compose:markup-header/")
752 # m2 might have been reordered by doc:replace.
754 m2['compose-type'] = 'attach'
755 m2['compose-info'] = str1
757 def handle_commit(self, key, focus, **a):
759 msg = email.message.EmailMessage()
760 m, l = self.vmarks(self.view)
762 # m is start of header marker, move to end.
770 focus.call("doc:file", 1, m2)
771 txt = focus.call("doc:get-str", m, m2, ret='str')
773 self.check_header("Date", email.utils.formatdate(localtime=True))
774 self.check_header("Message-id",
775 email.utils.make_msgid(
776 domain=focus['email:host-address']))
777 # discard empty headers
778 h = edlib.Mark(focus)
779 while self.find_empty_header(h):
780 focus.call("doc:EOL", -1, h)
782 focus.call("doc:EOL", 1, 1, h)
783 focus.call("doc:replace", h, h2)
785 # Now add all (non-empty) headers.
786 h = edlib.Mark(focus)
788 while self.find_any_header(h):
790 nm = self.this_header(h, he, downcase=False)
793 bdy = focus.call("doc:get-str", h, he, ret='str')
794 # any newline, together with surrounding blanks, becomes a space
795 # email.message doesn't like split headers.
796 bdy = ' '.join(re.split(r"[ \t]*\n[ \t]*",bdy)).strip()
799 if nm.lower() == "to" and not whoto:
801 n,a = email.utils.parseaddr(bdy)
810 self.call("Message", "No From: line in message - cannot send")
812 if not msg['subject']:
813 self.call("Message", "No Subject: line in message - cannot send")
815 if not msg['to'] and not msg['cc']:
816 self.call("Message", "No recipients (To: or Cc:) in message - cannot send")
819 # Look for attachments
823 marker = focus.call("doc:get-str", s, m, ret='str')
824 if marker and marker.startswith("@#!compose:attach "):
825 w = marker.strip().split(" ")
826 maintype = "application"
827 subtype = "octet-stream"
831 a = attr.split('=',1)
832 if a and len(a) == 2 and a[0] == "filename":
833 fn = urllib.parse.unquote(a[1])
834 bn=os.path.basename(fn)
836 (maintype, subtype) = a[1].split("/", 1)
839 with open(fn, 'rb') as fp:
840 if maintype == "message":
841 p = email.parser.BytesParser()
843 msg.add_attachment(eml)
845 msg.add_attachment(fp.read(), filename=bn,
849 self.call("Message", "Cannot read attachment %s" % fn)
853 tf = tempfile.TemporaryFile()
856 sendmail = focus['email:sendmail']
858 sendmail = "/sbin/sendmail -i"
860 p = subprocess.Popen(sendmail, shell=True,
862 stdout = subprocess.PIPE,
863 stderr = subprocess.PIPE)
867 focus.call("Message", "Failed to run sendmail command")
868 edlib.LOG("%s failed", sendmail)
872 focus.call("doc:set-name", "*Sent message to %s*" % whoto)
873 focus.call("doc:set:email-sent", "yes")
874 focus.call("doc:modified", -1)
875 fn = self['filename']
876 if fn and fn.startswith('/'):
879 except FileNotFoundError:
882 root = self.call("RootPane", ret='pane')
884 root.call("event:read", p.stdout.fileno(),
885 lambda key, **a: read_status(p, key, **a))
886 focus.call("Message", "Queueing message to %s." % whoto)
887 focus.call("Window:bury")
890 # Cannot find pane to report status on, so do it sync
891 out, err = p.communicate()
893 edlib.LOG("Email submission says:", out.decode('utf-8','ignore'))
895 focus.call("Message", "Email submission gives err: " +
896 err.decode('utf-8','ignore'))
897 focus.call("Message", "Email message to %s queued." % whoto)
898 focus.call("Window:bury")
902 def handle_spell(self, key, focus, mark, **a):
903 "handle:Spell:NextWord"
904 m2, l = self.vmarks(self.view)
905 if not mark or not m2 or not m2.next() or mark > m2:
906 return edlib.Efallthrough
909 h = self.this_header(mark.dup())
910 if h and h.lower() in ["subject"]:
911 return edlib.Efallthrough
912 found_end = not self.find_any_header(mark)
913 mark.to_mark(m2.next())
914 return edlib.Efallthrough
916 def handle_template(self, key, focus, **a):
918 # If document hasn't been modified, ask for a template name
920 # The external command "template" will list known templates, which
921 # can be given as an arg to produce the template.
922 mod = focus['doc-modified']
923 if mod and mod == 'yes':
924 focus.call("Message", "Compose template requires unchanged document.")
926 p = subprocess.Popen("template", shell=True,
927 stdout = subprocess.PIPE, stderr = subprocess.DEVNULL)
928 out, err = p.communicate()
930 focus.call("Message", "No known compose templates.")
933 doc = focus.call("doc:from-text", "*Choose Template*", out.decode(),
935 doc.call("doc:set:autoclose", 1)
936 pop = focus.call("PopupTile", "M1", ret='pane')
937 p = doc.call("doc:attach-view", pop, -1, ret='pane')
938 p['done-key'] = "Compose:Template"
939 p.call("attach-render-complete")
943 def handle_got_template(self, key, focus, str1, **a):
944 "handle:Compose:Template"
947 p = subprocess.Popen(["template", str1], stdout=subprocess.PIPE,
948 stderr = subprocess.DEVNULL)
949 out, err = p.communicate()
951 focus.call("Message", "No template provided.")
954 sep = out.find('\n\n')
962 f, l = self.vmarks(self.view)
963 focus.call("doc:replace", hdr, m, f)
967 focus.call("doc:replace", m, body)
968 focus.call("compose-email:empty-headers")
972 def compose_mode_attach(key, focus, comm2, **a):
973 focus['fill-width'] = '72'
974 p = focus.call("attach-textfill", ret='pane')
977 p = focus.call("attach-autospell", ret='pane')
980 p = compose_email(focus)
985 edlib.editor.call("global-set-command", "attach-compose-email", compose_mode_attach)