Skip to content

Commit bf95161

Browse files
committed
mbox: extract formatting into a shared package
Move the mbox generation logic out of pkg/web into a new pkg/mbox package so that both the web and API layers can use it. Replace the hand-rolled header parsing and formatting with go-mbox and go-message libraries: textproto.ReadHeader to parse stored headers, textproto.WriteHeader for output, mail.Address for address formatting, and mbox.Writer for the envelope framing. Normalize the body to CRLF line endings. Signed-off-by: Robin Jarry <robin@jarry.cc>
1 parent f484834 commit bf95161

5 files changed

Lines changed: 513 additions & 313 deletions

File tree

‎pkg/mbox/mbox.go‎

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// Patchwork - automated patch tracking system
2+
// Copyright (C) The Patchwork Contributors (see CONTRIBUTORS)
3+
//
4+
// SPDX-License-Identifier: GPL-2.0-or-later
5+
6+
package mbox
7+
8+
import (
9+
"bufio"
10+
"bytes"
11+
"context"
12+
"fmt"
13+
"regexp"
14+
"strconv"
15+
"strings"
16+
"time"
17+
18+
"github.com/emersion/go-mbox"
19+
"github.com/emersion/go-message/mail"
20+
"github.com/emersion/go-message/textproto"
21+
"github.com/uptrace/bun"
22+
23+
"github.com/getpatchwork/patchwork/pkg/db"
24+
"github.com/getpatchwork/patchwork/pkg/log"
25+
)
26+
27+
var (
28+
postscriptRe = regexp.MustCompile(`(?m)^-{2,3} ?$`)
29+
mboxResponseRe = regexp.MustCompile(`(?mi)^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by:.*$`)
30+
safeFilenameRe = regexp.MustCompile(`[^a-z0-9_.-]+`)
31+
)
32+
33+
type Submission struct {
34+
ID int
35+
Date time.Time
36+
Content string
37+
Diff string
38+
Headers string
39+
Submitter mail.Address
40+
DelegateEmail string
41+
ListEmail string
42+
CommentContents []string
43+
}
44+
45+
func Format(sub Submission) []byte {
46+
h, err := textproto.ReadHeader(bufio.NewReader(
47+
strings.NewReader(sub.Headers + "\n")))
48+
if err != nil {
49+
log.Errorf("textproto.ReadHeader: %v", err)
50+
h = textproto.Header{}
51+
}
52+
53+
if fromVal := h.Get("From"); fromVal != "" {
54+
fromAddr, err := mail.ParseAddress(fromVal)
55+
if err == nil && fromAddr.Address == sub.ListEmail {
56+
h.Set("X-Patchwork-Original-From", fromVal)
57+
h.Set("From", sub.Submitter.String())
58+
}
59+
}
60+
if !h.Has("Date") {
61+
h.Set("Date", sub.Date.UTC().Format(time.RFC1123Z))
62+
}
63+
h.Set("X-Patchwork-Submitter", sub.Submitter.String())
64+
h.Set("X-Patchwork-Id", strconv.Itoa(sub.ID))
65+
if sub.Diff != "" && sub.DelegateEmail != "" {
66+
h.Set("X-Patchwork-Delegate", sub.DelegateEmail)
67+
}
68+
h.Set("Content-Type", "text/plain; charset=utf-8")
69+
h.Set("Content-Transfer-Encoding", "8bit")
70+
71+
var buf bytes.Buffer
72+
73+
mw := mbox.NewWriter(&buf)
74+
75+
w, err := mw.CreateMessage("patchwork", sub.Date)
76+
if err != nil {
77+
log.Errorf("mbox.CreateMessage: %v", err)
78+
return nil
79+
}
80+
81+
if err := textproto.WriteHeader(w, h); err != nil {
82+
log.Errorf("textproto.WriteHeader: %v", err)
83+
return nil
84+
}
85+
86+
body := ""
87+
if sub.Content != "" {
88+
body = strings.TrimSpace(sub.Content) + "\n"
89+
}
90+
91+
postscript := ""
92+
if loc := postscriptRe.FindStringIndex(body); loc != nil {
93+
postscript = body[loc[1]:]
94+
body = strings.TrimSpace(body[:loc[0]]) + "\n"
95+
postscript = strings.TrimRight(postscript, " \t\n")
96+
}
97+
98+
for _, content := range sub.CommentContents {
99+
for _, m := range mboxResponseRe.FindAllString(content, -1) {
100+
body += m + "\n"
101+
}
102+
}
103+
104+
if postscript != "" {
105+
body += "---" + postscript + "\n"
106+
} else if sub.Diff != "" {
107+
body += "---\n"
108+
}
109+
110+
if sub.Diff != "" {
111+
body += "\n" + sub.Diff
112+
}
113+
114+
sc := bufio.NewScanner(strings.NewReader(body))
115+
for sc.Scan() {
116+
fmt.Fprintf(w, "%s\r\n", sc.Text())
117+
}
118+
mw.Close()
119+
120+
return buf.Bytes()
121+
}
122+
123+
func SanitizeFilename(name string) string {
124+
s := strings.ToLower(name)
125+
s = safeFilenameRe.ReplaceAllString(s, "-")
126+
s = strings.Trim(s, "-")
127+
s = strings.Trim(s, "-")
128+
if s == "" {
129+
s = "patch"
130+
}
131+
return s
132+
}
133+
134+
func BuildPatchSubmission(
135+
ctx context.Context, idb bun.IDB, patch *db.Patch, listEmail string,
136+
) Submission {
137+
sub := Submission{
138+
ID: patch.ID,
139+
Date: patch.Date,
140+
Headers: patch.Headers,
141+
ListEmail: listEmail,
142+
}
143+
if patch.Content != nil {
144+
sub.Content = *patch.Content
145+
}
146+
if patch.Diff != nil {
147+
sub.Diff = *patch.Diff
148+
}
149+
150+
var submitter db.Person
151+
if idb.NewSelect().Model(&submitter).
152+
Where("id = ?", patch.SubmitterID).
153+
Scan(ctx) == nil {
154+
sub.Submitter.Address = submitter.Email
155+
if submitter.Name != nil {
156+
sub.Submitter.Name = *submitter.Name
157+
}
158+
}
159+
160+
if patch.DelegateID != nil {
161+
var delegate db.User
162+
if idb.NewSelect().Model(&delegate).
163+
Where("id = ?", *patch.DelegateID).
164+
Scan(ctx) == nil {
165+
sub.DelegateEmail = delegate.Email
166+
}
167+
}
168+
169+
var contents []string
170+
if err := idb.NewSelect().
171+
Model((*db.PatchComment)(nil)).Column("content").
172+
Where("patch_id = ?", patch.ID).OrderExpr("date ASC").
173+
Scan(ctx, &contents); err != nil {
174+
log.Errorf("load patch comments: %v", err)
175+
}
176+
sub.CommentContents = contents
177+
178+
return sub
179+
}
180+
181+
func BuildCoverSubmission(
182+
ctx context.Context, idb bun.IDB, cover *db.Cover, listEmail string,
183+
) Submission {
184+
sub := Submission{
185+
ID: cover.ID,
186+
Date: cover.Date,
187+
Headers: cover.Headers,
188+
ListEmail: listEmail,
189+
}
190+
if cover.Content != nil {
191+
sub.Content = *cover.Content
192+
}
193+
194+
var submitter db.Person
195+
if idb.NewSelect().Model(&submitter).
196+
Where("id = ?", cover.SubmitterID).
197+
Scan(ctx) == nil {
198+
sub.Submitter.Address = submitter.Email
199+
if submitter.Name != nil {
200+
sub.Submitter.Name = *submitter.Name
201+
}
202+
}
203+
204+
var contents []string
205+
if err := idb.NewSelect().
206+
Model((*db.CoverComment)(nil)).Column("content").
207+
Where("cover_id = ?", cover.ID).OrderExpr("date ASC").
208+
Scan(ctx, &contents); err != nil {
209+
log.Errorf("load cover comments: %v", err)
210+
}
211+
sub.CommentContents = contents
212+
213+
return sub
214+
}

0 commit comments

Comments
 (0)