|
| 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