Blob


1 package apub
3 import (
4 "bytes"
5 "fmt"
6 "io"
7 "net/mail"
8 "net/smtp"
9 "strings"
10 "time"
11 )
13 func MarshalMail(activity *Activity) ([]byte, error) {
14 buf := &bytes.Buffer{}
16 from, err := LookupActor(activity.AttributedTo)
17 if err != nil {
18 return nil, fmt.Errorf("lookup actor %s: %w", activity.AttributedTo, err)
19 }
20 fmt.Fprintf(buf, "From: %s\n", from.Address())
22 var rcpt []string
23 for _, u := range activity.To {
24 if u == PublicCollection {
25 continue
26 }
27 actor, err := LookupActor(u)
28 if err != nil {
29 return nil, fmt.Errorf("lookup actor %s: %w", u, err)
30 }
31 rcpt = append(rcpt, actor.Address().String())
32 }
33 fmt.Fprintln(buf, "To:", strings.Join(rcpt, ", "))
35 var rcptcc []string
36 if activity.CC != nil {
37 for _, u := range activity.CC {
38 if u == PublicCollection {
39 continue
40 } else if u == from.Followers {
41 rcptcc = append(rcptcc, from.FollowersAddress().String())
42 continue
43 }
44 actor, err := LookupActor(u)
45 if err != nil {
46 return nil, fmt.Errorf("lookup actor %s: %w", u, err)
47 }
48 rcptcc = append(rcptcc, actor.Address().String())
49 }
50 fmt.Fprintln(buf, "CC:", strings.Join(rcptcc, ", "))
51 }
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)
57 }
58 if activity.InReplyTo != "" {
59 fmt.Fprintf(buf, "References: <%s>\n", activity.InReplyTo)
60 }
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)
66 } else {
67 fmt.Fprintln(buf, "Content-Type:", "text/html; charset=utf-8")
68 }
69 fmt.Fprintln(buf, "Subject:", activity.Name)
70 fmt.Fprintln(buf)
71 if activity.Source.Content != "" && activity.Source.MediaType == "text/markdown" {
72 fmt.Fprintln(buf, activity.Source.Content)
73 } else {
74 fmt.Fprintln(buf, activity.Content)
75 }
76 _, err = mail.ReadMessage(bytes.NewReader(buf.Bytes()))
77 return buf.Bytes(), err
78 }
80 func UnmarshalMail(msg *mail.Message) (*Activity, error) {
81 date, err := msg.Header.Date()
82 if err != nil {
83 return nil, fmt.Errorf("parse message date: %w", err)
84 }
85 from, err := msg.Header.AddressList("From")
86 if err != nil {
87 return nil, fmt.Errorf("parse From: %w", err)
88 }
89 wfrom, err := Finger(from[0].Address)
90 if err != nil {
91 return nil, fmt.Errorf("webfinger From: %w", err)
92 }
94 var wto, wcc []string
95 var tags []Activity
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.
100 if err != nil {
101 return nil, fmt.Errorf("parse To address list: %w", err)
103 actors, err := fingerAll(to)
104 if err != nil {
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}
112 wto[i] = a.ID
115 if msg.Header.Get("CC") != "" {
116 cc, err := msg.Header.AddressList("CC")
117 if err != nil {
118 return nil, fmt.Errorf("parse CC address list: %w", err)
120 actors, err := fingerAll(cc)
121 if err != nil {
122 return nil, fmt.Errorf("webfinger CC addresses: %w", err)
124 wcc = make([]string, len(actors))
125 for i, a := range actors {
126 wcc[i] = a.ID
130 buf := &bytes.Buffer{}
131 if _, err := io.Copy(buf, msg.Body); err != nil {
132 return nil, fmt.Errorf("read message body: %v", err)
135 return &Activity{
136 AtContext: NormContext,
137 Type: "Note",
138 AttributedTo: wfrom.ID,
139 To: wto,
140 CC: wcc,
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"), "<>"),
145 Published: &date,
146 Tag: tags,
147 }, nil
150 func SendMail(addr string, auth smtp.Auth, from string, to []string, activity *Activity) error {
151 msg, err := MarshalMail(activity)
152 if err != nil {
153 return fmt.Errorf("marshal to mail message: %w", err)
155 return smtp.SendMail(addr, auth, from, to, msg)