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.
 
 
 
 

211 lines
4.4 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. from := []*mail.Address{{"", msg.From}}
  105. to := make([]*mail.Address, len(msg.To))
  106. for i, addr := range msg.To {
  107. to[i] = &mail.Address{"", addr}
  108. }
  109. var h mail.Header
  110. h.SetDate(time.Now())
  111. h.SetAddressList("From", from)
  112. h.SetAddressList("To", to)
  113. if msg.Subject != "" {
  114. h.SetText("Subject", msg.Subject)
  115. }
  116. if msg.InReplyTo != "" {
  117. h.Set("In-Reply-To", msg.InReplyTo)
  118. }
  119. h.Set("Message-Id", msg.MessageID)
  120. if msg.MessageID == "" {
  121. panic(fmt.Errorf("Attempting to send message without message ID"))
  122. }
  123. mw, err := mail.CreateWriter(w, h)
  124. if err != nil {
  125. return fmt.Errorf("failed to create mail writer: %v", err)
  126. }
  127. var th mail.InlineHeader
  128. th.Set("Content-Type", "text/plain; charset=utf-8")
  129. tw, err := mw.CreateSingleInline(th)
  130. if err != nil {
  131. return fmt.Errorf("failed to create text part: %v", err)
  132. }
  133. defer tw.Close()
  134. if _, err := io.WriteString(tw, msg.Text); err != nil {
  135. return fmt.Errorf("failed to write text part: %v", err)
  136. }
  137. if err := tw.Close(); err != nil {
  138. return fmt.Errorf("failed to close text part: %v", err)
  139. }
  140. for _, att := range msg.Attachments {
  141. if err := writeAttachment(mw, att); err != nil {
  142. return err
  143. }
  144. }
  145. if err := mw.Close(); err != nil {
  146. return fmt.Errorf("failed to close mail writer: %v", err)
  147. }
  148. return nil
  149. }
  150. func sendMessage(c *smtp.Client, msg *OutgoingMessage) error {
  151. if err := c.Mail(msg.From, nil); err != nil {
  152. return fmt.Errorf("MAIL FROM failed: %v", err)
  153. }
  154. for _, to := range msg.To {
  155. if err := c.Rcpt(to); err != nil {
  156. return fmt.Errorf("RCPT TO failed: %v", err)
  157. }
  158. }
  159. w, err := c.Data()
  160. if err != nil {
  161. return fmt.Errorf("DATA failed: %v", err)
  162. }
  163. defer w.Close()
  164. if err := msg.WriteTo(w); err != nil {
  165. return fmt.Errorf("failed to write outgoing message: %v", err)
  166. }
  167. if err := w.Close(); err != nil {
  168. return fmt.Errorf("failed to close SMTP data writer: %v", err)
  169. }
  170. return nil
  171. }