A webmail client. Forked from https://git.sr.ht/~migadu/alps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

221 lines
4.7 KiB

  1. package alpsbase
  2. import (
  3. "bufio"
  4. "bytes"
  5. "fmt"
  6. "io"
  7. "io/ioutil"
  8. "mime"
  9. "mime/multipart"
  10. "strings"
  11. "time"
  12. "github.com/emersion/go-message/mail"
  13. "github.com/emersion/go-smtp"
  14. )
  15. func quote(r io.Reader) (string, error) {
  16. scanner := bufio.NewScanner(r)
  17. var builder strings.Builder
  18. for scanner.Scan() {
  19. builder.WriteString("> ")
  20. builder.Write(scanner.Bytes())
  21. builder.WriteString("\n")
  22. }
  23. if err := scanner.Err(); err != nil {
  24. return "", fmt.Errorf("quote: failed to read original message: %s", err)
  25. }
  26. builder.WriteString("\n")
  27. return builder.String(), nil
  28. }
  29. type Attachment interface {
  30. MIMEType() string
  31. Filename() string
  32. Open() (io.ReadCloser, error)
  33. }
  34. type formAttachment struct {
  35. *multipart.FileHeader
  36. }
  37. func (att *formAttachment) Open() (io.ReadCloser, error) {
  38. return att.FileHeader.Open()
  39. }
  40. func (att *formAttachment) MIMEType() string {
  41. // TODO: retain params, e.g. "charset"?
  42. t, _, _ := mime.ParseMediaType(att.FileHeader.Header.Get("Content-Type"))
  43. return t
  44. }
  45. func (att *formAttachment) Filename() string {
  46. return att.FileHeader.Filename
  47. }
  48. type imapAttachment struct {
  49. Mailbox string
  50. Uid uint32
  51. Node *IMAPPartNode
  52. Body []byte
  53. }
  54. func (att *imapAttachment) Open() (io.ReadCloser, error) {
  55. if att.Body == nil {
  56. return nil, fmt.Errorf("IMAP attachment has not been pre-fetched")
  57. }
  58. return ioutil.NopCloser(bytes.NewReader(att.Body)), nil
  59. }
  60. func (att *imapAttachment) MIMEType() string {
  61. return att.Node.MIMEType
  62. }
  63. func (att *imapAttachment) Filename() string {
  64. return att.Node.Filename
  65. }
  66. type OutgoingMessage struct {
  67. From string
  68. To []string
  69. Subject string
  70. MessageID string
  71. InReplyTo string
  72. Text string
  73. Attachments []Attachment
  74. }
  75. func (msg *OutgoingMessage) ToString() string {
  76. return strings.Join(msg.To, ", ")
  77. }
  78. func writeAttachment(mw *mail.Writer, att Attachment) error {
  79. var h mail.AttachmentHeader
  80. h.SetContentType(att.MIMEType(), nil)
  81. h.SetFilename(att.Filename())
  82. aw, err := mw.CreateAttachment(h)
  83. if err != nil {
  84. return fmt.Errorf("failed to create attachment: %v", err)
  85. }
  86. defer aw.Close()
  87. f, err := att.Open()
  88. if err != nil {
  89. return fmt.Errorf("failed to open attachment: %v", err)
  90. }
  91. defer f.Close()
  92. if _, err := io.Copy(aw, f); err != nil {
  93. return fmt.Errorf("failed to write attachment: %v", err)
  94. }
  95. if err := f.Close(); err != nil {
  96. return fmt.Errorf("failed to close attachment: %v", err)
  97. }
  98. if err := aw.Close(); err != nil {
  99. return fmt.Errorf("failed to close attachment writer: %v", err)
  100. }
  101. return nil
  102. }
  103. func (msg *OutgoingMessage) WriteTo(w io.Writer) error {
  104. fromAddr, err := mail.ParseAddress(msg.From)
  105. if err != nil {
  106. return err
  107. }
  108. from := []*mail.Address{fromAddr}
  109. to := make([]*mail.Address, len(msg.To))
  110. for i, rcpt := range msg.To {
  111. addr, err := mail.ParseAddress(rcpt)
  112. if err != nil {
  113. return err
  114. }
  115. to[i] = addr
  116. }
  117. var h mail.Header
  118. h.SetDate(time.Now())
  119. h.SetAddressList("From", from)
  120. h.SetAddressList("To", to)
  121. if msg.Subject != "" {
  122. h.SetText("Subject", msg.Subject)
  123. }
  124. if msg.InReplyTo != "" {
  125. h.Set("In-Reply-To", msg.InReplyTo)
  126. }
  127. h.Set("Message-Id", msg.MessageID)
  128. if msg.MessageID == "" {
  129. panic(fmt.Errorf("Attempting to send message without message ID"))
  130. }
  131. mw, err := mail.CreateWriter(w, h)
  132. if err != nil {
  133. return fmt.Errorf("failed to create mail writer: %v", err)
  134. }
  135. var th mail.InlineHeader
  136. th.Set("Content-Type", "text/plain; charset=utf-8")
  137. tw, err := mw.CreateSingleInline(th)
  138. if err != nil {
  139. return fmt.Errorf("failed to create text part: %v", err)
  140. }
  141. defer tw.Close()
  142. if _, err := io.WriteString(tw, msg.Text); err != nil {
  143. return fmt.Errorf("failed to write text part: %v", err)
  144. }
  145. if err := tw.Close(); err != nil {
  146. return fmt.Errorf("failed to close text part: %v", err)
  147. }
  148. for _, att := range msg.Attachments {
  149. if err := writeAttachment(mw, att); err != nil {
  150. return err
  151. }
  152. }
  153. if err := mw.Close(); err != nil {
  154. return fmt.Errorf("failed to close mail writer: %v", err)
  155. }
  156. return nil
  157. }
  158. func sendMessage(c *smtp.Client, msg *OutgoingMessage) error {
  159. addr, _ := mail.ParseAddress(msg.From)
  160. if err := c.Mail(addr.Address, nil); err != nil {
  161. return fmt.Errorf("MAIL FROM failed: %v", err)
  162. }
  163. for _, to := range msg.To {
  164. addr, _ := mail.ParseAddress(to)
  165. if err := c.Rcpt(addr.Address); err != nil {
  166. return fmt.Errorf("RCPT TO failed: %v (%s)", err, addr.Address)
  167. }
  168. }
  169. w, err := c.Data()
  170. if err != nil {
  171. return fmt.Errorf("DATA failed: %v", err)
  172. }
  173. defer w.Close()
  174. if err := msg.WriteTo(w); err != nil {
  175. return fmt.Errorf("failed to write outgoing message: %v", err)
  176. }
  177. if err := w.Close(); err != nil {
  178. return fmt.Errorf("failed to close SMTP data writer: %v", err)
  179. }
  180. return nil
  181. }