14 func MarshalMail(activity *Activity, client *Client) ([]byte, error) {
15 msg, err := marshalMail(activity, client)
19 return encodeMsg(msg), nil
22 func marshalMail(activity *Activity, client *Client) (*mail.Message, error) {
24 client = &DefaultClient
27 msg := new(mail.Message)
28 msg.Header = make(mail.Header)
30 from, err := client.LookupActor(activity.AttributedTo)
32 return nil, fmt.Errorf("build From: lookup actor %s: %w", activity.AttributedTo, err)
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 {
43 a, err := client.LookupActor(id)
45 return nil, fmt.Errorf("build To: lookup actor %s: %w", id, err)
47 if a.Type == "Collection" || a.Type == "OrderedCollection" {
48 collections = append(collections, a.ID)
50 addrs = append(addrs, a.Address().String())
51 actors = append(actors, *a)
54 for _, id := range collections {
55 if i := indexFollowers(actors, id); i >= 0 {
56 addrs = append(addrs, actors[i].FollowersAddress().String())
59 msg.Header["To"] = addrs
61 addrs, collections = []string{}, []string{}
62 for _, id := range activity.CC {
63 if id == PublicCollection {
67 a, err := client.LookupActor(id)
69 return nil, fmt.Errorf("build CC: lookup actor %s: %w", id, err)
71 if a.Type == "Collection" || a.Type == "OrderedCollection" {
72 collections = append(collections, a.ID)
75 addrs = append(addrs, a.Address().String())
76 actors = append(actors, *a)
78 for _, id := range collections {
79 if i := indexFollowers(actors, id); i >= 0 {
80 addrs = append(addrs, actors[i].FollowersAddress().String())
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 + ">"}
91 if activity.InReplyTo != "" {
92 msg.Header["In-Reply-To"] = []string{"<" + activity.InReplyTo + ">"}
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"}
106 func indexFollowers(actors []Actor, id string) int {
107 for i := range actors {
108 if actors[i].Followers == id {
115 func UnmarshalMail(msg *mail.Message, client *Client) (*Activity, error) {
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()
126 return nil, fmt.Errorf("parse message date: %w", err)
128 from, err := msg.Header.AddressList("From")
130 return nil, fmt.Errorf("parse From: %w", err)
132 wfrom, err := client.Finger(from[0].Address)
134 return nil, fmt.Errorf("webfinger From: %w", err)
137 var wto, wcc []string
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.
144 return nil, fmt.Errorf("parse To address list: %w", err)
146 actors, err := client.fingerAll(to)
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") {
157 tags = append(tags, Activity{Type: "Mention", Href: a.ID, Name: "@" + addr})
161 if msg.Header.Get("CC") != "" {
162 cc, err := msg.Header.AddressList("CC")
164 return nil, fmt.Errorf("parse CC address list: %w", err)
166 actors, err := client.fingerAll(cc)
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") {
180 buf := &bytes.Buffer{}
181 if msg.Header.Get("Content-Transfer-Encoding") == "quoted-printable" {
182 _, err = io.Copy(buf, quotedprintable.NewReader(msg.Body))
184 _, err = io.Copy(buf, msg.Body)
187 return nil, fmt.Errorf("read message body: %v", err)
189 content := strings.TrimSpace(strings.ReplaceAll(buf.String(), "\r", ""))
192 AtContext: NormContext,
194 AttributedTo: wfrom.ID,
197 MediaType: "text/markdown",
198 Name: strings.TrimSpace(msg.Header.Get("Subject")),
200 InReplyTo: strings.Trim(msg.Header.Get("In-Reply-To"), "<>"),
206 func SendMail(addr string, auth smtp.Auth, from string, to []string, activity *Activity) error {
207 msg, err := MarshalMail(activity, 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 {
220 case "Subject", "From":
223 fmt.Fprintf(buf, "%s: %s\n", k, strings.Join(v, ", "))
226 fmt.Fprintln(buf, "Subject:", msg.Header.Get("Subject"))
228 io.Copy(buf, msg.Body)