13 func MarshalMail(activity *Activity) ([]byte, error) {
14 buf := &bytes.Buffer{}
16 from, err := LookupActor(activity.AttributedTo)
18 return nil, fmt.Errorf("lookup actor %s: %w", activity.AttributedTo, err)
20 fmt.Fprintf(buf, "From: %s\n", from.Address())
23 for _, u := range activity.To {
24 if u == PublicCollection {
27 actor, err := LookupActor(u)
29 return nil, fmt.Errorf("lookup actor %s: %w", u, err)
31 rcpt = append(rcpt, actor.Address().String())
33 fmt.Fprintln(buf, "To:", strings.Join(rcpt, ", "))
36 if activity.CC != nil {
37 for _, u := range activity.CC {
38 if u == PublicCollection {
40 } else if u == from.Followers {
41 rcptcc = append(rcptcc, from.FollowersAddress().String())
44 actor, err := LookupActor(u)
46 return nil, fmt.Errorf("lookup actor %s: %w", u, err)
48 rcptcc = append(rcptcc, actor.Address().String())
50 fmt.Fprintln(buf, "CC:", strings.Join(rcptcc, ", "))
53 fmt.Fprintf(buf, "Date: %s\n", activity.Published.Format(time.RFC822))
54 fmt.Fprintf(buf, "Message-ID: <%s>\n", activity.ID)
55 if activity.Audience != "" {
56 fmt.Fprintf(buf, "List-ID: <%s>\n", activity.Audience)
58 if activity.InReplyTo != "" {
59 fmt.Fprintf(buf, "References: <%s>\n", activity.InReplyTo)
62 if activity.Source.Content != "" && activity.Source.MediaType == "text/markdown" {
63 fmt.Fprintln(buf, "Content-Type: text/plain; charset=utf-8")
64 } else if activity.MediaType != "" {
65 fmt.Fprintln(buf, "Content-Type:", activity.MediaType)
67 fmt.Fprintln(buf, "Content-Type:", "text/html; charset=utf-8")
69 fmt.Fprintln(buf, "Subject:", activity.Name)
71 if activity.Source.Content != "" && activity.Source.MediaType == "text/markdown" {
72 fmt.Fprintln(buf, activity.Source.Content)
74 fmt.Fprintln(buf, activity.Content)
76 _, err = mail.ReadMessage(bytes.NewReader(buf.Bytes()))
77 return buf.Bytes(), err
80 func UnmarshalMail(msg *mail.Message) (*Activity, error) {
81 date, err := msg.Header.Date()
83 return nil, fmt.Errorf("parse message date: %w", err)
85 from, err := msg.Header.AddressList("From")
87 return nil, fmt.Errorf("parse From: %w", err)
89 wfrom, err := Finger(from[0].Address)
91 return nil, fmt.Errorf("webfinger From: %w", err)
96 if msg.Header.Get("To") != "" {
97 to, err := msg.Header.AddressList("To")
98 // ignore missing To line. Some ActivityPub servers only have the
99 // PublicCollection listed, which we don't care about.
101 return nil, fmt.Errorf("parse To address list: %w", err)
103 actors, err := fingerAll(to)
105 return nil, fmt.Errorf("webfinger To addresses: %w", err)
107 wto = make([]string, len(actors))
108 tags = make([]Activity, len(actors))
109 for i, a := range actors {
110 addr := strings.Trim(to[i].Address, "<>")
111 tags[i] = Activity{Type: "Mention", Href: a.ID, Name: "@" + addr}
115 if msg.Header.Get("CC") != "" {
116 cc, err := msg.Header.AddressList("CC")
118 return nil, fmt.Errorf("parse CC address list: %w", err)
120 actors, err := fingerAll(cc)
122 return nil, fmt.Errorf("webfinger CC addresses: %w", err)
124 wcc = make([]string, len(actors))
125 for i, a := range actors {
130 buf := &bytes.Buffer{}
131 if _, err := io.Copy(buf, msg.Body); err != nil {
132 return nil, fmt.Errorf("read message body: %v", err)
136 AtContext: NormContext,
138 AttributedTo: wfrom.ID,
141 MediaType: "text/markdown",
142 Name: strings.TrimSpace(msg.Header.Get("Subject")),
143 Content: strings.TrimSpace(buf.String()),
144 InReplyTo: strings.Trim(msg.Header.Get("In-Reply-To"), "<>"),
150 func SendMail(addr string, auth smtp.Auth, from string, to []string, activity *Activity) error {
151 msg, err := MarshalMail(activity)
153 return fmt.Errorf("marshal to mail message: %w", err)
155 return smtp.SendMail(addr, auth, from, to, msg)