13 func MarshalMail(activity *Activity, client *Client) ([]byte, error) {
14 msg, err := marshalMail(activity, client)
18 return encodeMsg(msg), nil
21 func marshalMail(activity *Activity, client *Client) (*mail.Message, error) {
23 client = &DefaultClient
26 msg := new(mail.Message)
27 msg.Header = make(mail.Header)
29 from, err := client.LookupActor(activity.AttributedTo)
31 return nil, fmt.Errorf("build From: lookup actor %s: %w", activity.AttributedTo, err)
33 actors = append(actors, *from)
34 msg.Header["From"] = []string{from.Address().String()}
36 var addrs, collections []string
37 for _, id := range activity.To {
38 if id == PublicCollection {
42 a, err := client.LookupActor(id)
44 return nil, fmt.Errorf("build To: lookup actor %s: %w", id, err)
46 if a.Type == "Collection" || a.Type == "OrderedCollection" {
47 collections = append(collections, a.ID)
49 addrs = append(addrs, a.Address().String())
50 actors = append(actors, *a)
53 for _, id := range collections {
54 if i := indexFollowers(actors, id); i >= 0 {
55 addrs = append(addrs, actors[i].FollowersAddress().String())
58 msg.Header["To"] = addrs
60 addrs, collections = []string{}, []string{}
61 for _, id := range activity.CC {
62 if id == PublicCollection {
66 a, err := client.LookupActor(id)
68 return nil, fmt.Errorf("build CC: lookup actor %s: %w", id, err)
70 if a.Type == "Collection" || a.Type == "OrderedCollection" {
71 collections = append(collections, a.ID)
74 addrs = append(addrs, a.Address().String())
75 actors = append(actors, *a)
77 for _, id := range collections {
78 if i := indexFollowers(actors, id); i >= 0 {
79 addrs = append(addrs, actors[i].FollowersAddress().String())
82 msg.Header["CC"] = addrs
84 msg.Header["Date"] = []string{activity.Published.Format(time.RFC1123Z)}
85 msg.Header["Message-ID"] = []string{"<" + activity.ID + ">"}
86 msg.Header["Subject"] = []string{activity.Name}
87 if activity.Audience != "" {
88 msg.Header["List-ID"] = []string{"<" + activity.Audience + ">"}
90 if activity.InReplyTo != "" {
91 msg.Header["In-Reply-To"] = []string{"<" + activity.InReplyTo + ">"}
94 msg.Body = strings.NewReader(activity.Content)
95 msg.Header["Content-Type"] = []string{"text/html; charset=utf-8"}
96 if activity.Source.Content != "" && activity.Source.MediaType == "text/markdown" {
97 msg.Body = strings.NewReader(activity.Source.Content)
98 msg.Header["Content-Type"] = []string{"text/plain; charset=utf-8"}
99 } else if activity.MediaType == "text/markdown" {
100 msg.Header["Content-Type"] = []string{"text/plain; charset=utf-8"}
105 func indexFollowers(actors []Actor, id string) int {
106 for i := range actors {
107 if actors[i].Followers == id {
114 func UnmarshalMail(msg *mail.Message, client *Client) (*Activity, error) {
116 client = &DefaultClient
118 ct := msg.Header.Get("Content-Type")
119 if strings.HasPrefix(ct, "multipart") {
120 return nil, fmt.Errorf("cannot unmarshal from multipart message")
122 enc := msg.Header.Get("Content-Transfer-Encoding")
123 if enc == "quoted-printable" {
124 return nil, fmt.Errorf("cannot decode message with transfer encoding: %s", enc)
127 date, err := msg.Header.Date()
129 return nil, fmt.Errorf("parse message date: %w", err)
131 from, err := msg.Header.AddressList("From")
133 return nil, fmt.Errorf("parse From: %w", err)
135 wfrom, err := client.Finger(from[0].Address)
137 return nil, fmt.Errorf("webfinger From: %w", err)
140 var wto, wcc []string
142 if msg.Header.Get("To") != "" {
143 to, err := msg.Header.AddressList("To")
144 // ignore missing To line. Some ActivityPub servers only have the
145 // PublicCollection listed, which we don't care about.
147 return nil, fmt.Errorf("parse To address list: %w", err)
149 actors, err := client.fingerAll(to)
151 return nil, fmt.Errorf("webfinger To addresses: %w", err)
153 wto = make([]string, len(actors))
154 for i, a := range actors {
155 addr := strings.Trim(to[i].Address, "<>")
156 if strings.Contains(addr, "+followers") {
160 tags = append(tags, Activity{Type: "Mention", Href: a.ID, Name: "@" + addr})
164 if msg.Header.Get("CC") != "" {
165 cc, err := msg.Header.AddressList("CC")
167 return nil, fmt.Errorf("parse CC address list: %w", err)
169 actors, err := client.fingerAll(cc)
171 return nil, fmt.Errorf("webfinger CC addresses: %w", err)
173 wcc = make([]string, len(actors))
174 for i, a := range actors {
175 if strings.Contains(cc[i].Address, "+followers") {
183 buf := &bytes.Buffer{}
184 if _, err := io.Copy(buf, msg.Body); err != nil {
185 return nil, fmt.Errorf("read message body: %v", err)
187 content := strings.TrimSpace(strings.ReplaceAll(buf.String(), "\r", ""))
190 AtContext: NormContext,
192 AttributedTo: wfrom.ID,
195 MediaType: "text/markdown",
196 Name: strings.TrimSpace(msg.Header.Get("Subject")),
198 InReplyTo: strings.Trim(msg.Header.Get("In-Reply-To"), "<>"),
204 func SendMail(addr string, auth smtp.Auth, from string, to []string, activity *Activity) error {
205 msg, err := MarshalMail(activity, nil)
207 return fmt.Errorf("marshal to mail message: %w", err)
209 return smtp.SendMail(addr, auth, from, to, msg)
212 func encodeMsg(msg *mail.Message) []byte {
213 buf := &bytes.Buffer{}
214 // Lead with "From", end with "Subject" to make some mail clients happy.
215 fmt.Fprintln(buf, "From:", msg.Header.Get("From"))
216 for k, v := range msg.Header {
218 case "Subject", "From":
221 fmt.Fprintf(buf, "%s: %s\n", k, strings.Join(v, ", "))
224 fmt.Fprintln(buf, "Subject:", msg.Header.Get("Subject"))
226 io.Copy(buf, msg.Body)