Blob


1 package apub
3 import (
4 "bytes"
5 "fmt"
6 "io"
7 "mime/quotedprintable"
8 "net/mail"
9 "net/smtp"
10 "strings"
11 "time"
12 )
14 func MarshalMail(activity *Activity, client *Client) ([]byte, error) {
15 msg, err := marshalMail(activity, client)
16 if err != nil {
17 return nil, err
18 }
19 return encodeMsg(msg), nil
20 }
22 func marshalMail(activity *Activity, client *Client) (*mail.Message, error) {
23 if client == nil {
24 client = &DefaultClient
25 }
27 msg := new(mail.Message)
28 msg.Header = make(mail.Header)
29 var actors []Actor
30 from, err := client.LookupActor(activity.AttributedTo)
31 if err != nil {
32 return nil, fmt.Errorf("build From: lookup actor %s: %w", activity.AttributedTo, err)
33 }
34 actors = append(actors, *from)
35 msg.Header["From"] = []string{from.Address().String()}
37 var addrs, collections []string
38 for _, id := range activity.To {
39 if id == PublicCollection {
40 continue
41 }
43 a, err := client.LookupActor(id)
44 if err != nil {
45 return nil, fmt.Errorf("build To: lookup actor %s: %w", id, err)
46 }
47 if a.Type == "Collection" || a.Type == "OrderedCollection" {
48 collections = append(collections, a.ID)
49 } else {
50 addrs = append(addrs, a.Address().String())
51 actors = append(actors, *a)
52 }
53 }
54 for _, id := range collections {
55 if i := indexFollowers(actors, id); i >= 0 {
56 addrs = append(addrs, actors[i].FollowersAddress().String())
57 }
58 }
59 msg.Header["To"] = addrs
61 addrs, collections = []string{}, []string{}
62 for _, id := range activity.CC {
63 if id == PublicCollection {
64 continue
65 }
67 a, err := client.LookupActor(id)
68 if err != nil {
69 return nil, fmt.Errorf("build CC: lookup actor %s: %w", id, err)
70 }
71 if a.Type == "Collection" || a.Type == "OrderedCollection" {
72 collections = append(collections, a.ID)
73 continue
74 }
75 addrs = append(addrs, a.Address().String())
76 actors = append(actors, *a)
77 }
78 for _, id := range collections {
79 if i := indexFollowers(actors, id); i >= 0 {
80 addrs = append(addrs, actors[i].FollowersAddress().String())
81 }
82 }
83 msg.Header["CC"] = addrs
85 msg.Header["Date"] = []string{activity.Published.Format(time.RFC1123Z)}
86 msg.Header["Message-ID"] = []string{"<" + activity.ID + ">"}
87 msg.Header["Subject"] = []string{activity.Name}
88 if activity.Audience != "" {
89 msg.Header["List-ID"] = []string{"<" + activity.Audience + ">"}
90 }
91 if activity.InReplyTo != "" {
92 msg.Header["In-Reply-To"] = []string{"<" + activity.InReplyTo + ">"}
93 }
95 msg.Body = strings.NewReader(activity.Content)
96 msg.Header["Content-Type"] = []string{"text/html; charset=utf-8"}
97 if activity.Source.Content != "" && activity.Source.MediaType == "text/markdown" {
98 msg.Body = strings.NewReader(activity.Source.Content)
99 msg.Header["Content-Type"] = []string{"text/plain; charset=utf-8"}
100 } else if activity.MediaType == "text/markdown" {
101 msg.Header["Content-Type"] = []string{"text/plain; charset=utf-8"}
103 return msg, nil
106 func indexFollowers(actors []Actor, id string) int {
107 for i := range actors {
108 if actors[i].Followers == id {
109 return i
112 return -1
115 func UnmarshalMail(msg *mail.Message, client *Client) (*Activity, error) {
116 if client == nil {
117 client = &DefaultClient
119 ct := msg.Header.Get("Content-Type")
120 if strings.HasPrefix(ct, "multipart") {
121 return nil, fmt.Errorf("cannot unmarshal from multipart message")
124 date, err := msg.Header.Date()
125 if err != nil {
126 return nil, fmt.Errorf("parse message date: %w", err)
128 from, err := msg.Header.AddressList("From")
129 if err != nil {
130 return nil, fmt.Errorf("parse From: %w", err)
132 wfrom, err := client.Finger(from[0].Address)
133 if err != nil {
134 return nil, fmt.Errorf("webfinger From: %w", err)
137 var wto, wcc []string
138 var tags []Activity
139 if msg.Header.Get("To") != "" {
140 to, err := msg.Header.AddressList("To")
141 // ignore missing To line. Some ActivityPub servers only have the
142 // PublicCollection listed, which we don't care about.
143 if err != nil {
144 return nil, fmt.Errorf("parse To address list: %w", err)
146 actors, err := client.fingerAll(to)
147 if err != nil {
148 return nil, fmt.Errorf("webfinger To addresses: %w", err)
150 wto = make([]string, len(actors))
151 for i, a := range actors {
152 addr := strings.Trim(to[i].Address, "<>")
153 if strings.Contains(addr, "+followers") {
154 wto[i] = a.Followers
155 continue
157 tags = append(tags, Activity{Type: "Mention", Href: a.ID, Name: "@" + addr})
158 wto[i] = a.ID
161 if msg.Header.Get("CC") != "" {
162 cc, err := msg.Header.AddressList("CC")
163 if err != nil {
164 return nil, fmt.Errorf("parse CC address list: %w", err)
166 actors, err := client.fingerAll(cc)
167 if err != nil {
168 return nil, fmt.Errorf("webfinger CC addresses: %w", err)
170 wcc = make([]string, len(actors))
171 for i, a := range actors {
172 if strings.Contains(cc[i].Address, "+followers") {
173 wcc[i] = a.Followers
174 continue
176 wcc[i] = a.ID
180 buf := &bytes.Buffer{}
181 if msg.Header.Get("Content-Transfer-Encoding") == "quoted-printable" {
182 _, err = io.Copy(buf, quotedprintable.NewReader(msg.Body))
183 } else {
184 _, err = io.Copy(buf, msg.Body)
186 if err != nil {
187 return nil, fmt.Errorf("read message body: %v", err)
189 content := strings.TrimSpace(strings.ReplaceAll(buf.String(), "\r", ""))
191 return &Activity{
192 AtContext: NormContext,
193 Type: "Note",
194 AttributedTo: wfrom.ID,
195 To: wto,
196 CC: wcc,
197 MediaType: "text/markdown",
198 Name: strings.TrimSpace(msg.Header.Get("Subject")),
199 Content: content,
200 InReplyTo: strings.Trim(msg.Header.Get("In-Reply-To"), "<>"),
201 Published: &date,
202 Tag: tags,
203 }, nil
206 func SendMail(addr string, auth smtp.Auth, from string, to []string, activity *Activity) error {
207 msg, err := MarshalMail(activity, nil)
208 if err != nil {
209 return fmt.Errorf("marshal to mail message: %w", err)
211 return smtp.SendMail(addr, auth, from, to, msg)
214 func encodeMsg(msg *mail.Message) []byte {
215 buf := &bytes.Buffer{}
216 // Lead with "From", end with "Subject" to make some mail clients happy.
217 fmt.Fprintln(buf, "From:", msg.Header.Get("From"))
218 for k, v := range msg.Header {
219 switch k {
220 case "Subject", "From":
221 continue
222 default:
223 fmt.Fprintf(buf, "%s: %s\n", k, strings.Join(v, ", "))
226 fmt.Fprintln(buf, "Subject:", msg.Header.Get("Subject"))
227 fmt.Fprintln(buf)
228 io.Copy(buf, msg.Body)
229 return buf.Bytes()