github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/website/email.go (about) 1 /* 2 Copyright 2013 Google Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bytes" 21 "flag" 22 "fmt" 23 "log" 24 "net/http" 25 "net/smtp" 26 "os" 27 "os/exec" 28 "strings" 29 "sync" 30 "time" 31 32 "camlistore.org/pkg/osutil" 33 ) 34 35 var ( 36 emailNow = flag.String("email_now", "", "If non-empty, this commit hash is emailed immediately, without starting the webserver.") 37 smtpServer = flag.String("smtp_server", "127.0.0.1:25", "SMTP server") 38 emailsTo = flag.String("email_dest", "", "If non-empty, the email address to email commit emails.") 39 ) 40 41 func startEmailCommitLoop(errc chan<- error) { 42 if *emailsTo == "" { 43 return 44 } 45 if *emailNow != "" { 46 dir, err := osutil.GoPackagePath("camlistore.org") 47 if err != nil { 48 log.Fatal(err) 49 } 50 if err := emailCommit(dir, *emailNow); err != nil { 51 log.Fatal(err) 52 } 53 os.Exit(0) 54 } 55 go func() { 56 errc <- commitEmailLoop() 57 }() 58 } 59 60 // tokenc holds tokens for the /mailnow handler. 61 // Hitting /mailnow (unauthenticated) forces a 'git fetch origin 62 // master'. Because it's unauthenticated, we don't want to allow 63 // attackers to force us to hit git. The /mailnow handler tries to 64 // take a token from tokenc. 65 var tokenc = make(chan bool, 3) 66 67 var fetchc = make(chan bool, 1) 68 69 var knownCommit = map[string]bool{} // commit -> true 70 71 var diffMarker = []byte("diff --git a/") 72 73 func emailCommit(dir, hash string) (err error) { 74 cmd := exec.Command("git", "show", hash) 75 cmd.Dir = dir 76 body, err := cmd.CombinedOutput() 77 if err != nil { 78 return fmt.Errorf("Error runnning git show: %v\n%s", err, body) 79 } 80 if !bytes.Contains(body, diffMarker) { 81 // Boring merge commit. Don't email. 82 return nil 83 } 84 85 cmd = exec.Command("git", "show", "--pretty=oneline", hash) 86 cmd.Dir = dir 87 out, err := cmd.Output() 88 if err != nil { 89 return 90 } 91 subj := out[41:] // remove hash and space 92 if i := bytes.IndexByte(subj, '\n'); i != -1 { 93 subj = subj[:i] 94 } 95 if len(subj) > 80 { 96 subj = subj[:80] 97 } 98 99 cl, err := smtp.Dial(*smtpServer) 100 if err != nil { 101 return 102 } 103 defer cl.Quit() 104 if err = cl.Mail("noreply@camlistore.org"); err != nil { 105 return 106 } 107 if err = cl.Rcpt(*emailsTo); err != nil { 108 return 109 } 110 wc, err := cl.Data() 111 if err != nil { 112 return 113 } 114 _, err = fmt.Fprintf(wc, `From: noreply@camlistore.org (Camlistore Commit) 115 To: %s 116 Subject: %s 117 Reply-To: camlistore@googlegroups.com 118 119 https://camlistore.googlesource.com/camlistore/+/%s 120 121 %s`, *emailsTo, subj, hash, body) 122 if err != nil { 123 return 124 } 125 return wc.Close() 126 } 127 128 var latestHash struct { 129 sync.Mutex 130 s string // hash of the most recent camlistore revision 131 } 132 133 func commitEmailLoop() error { 134 http.HandleFunc("/mailnow", mailNowHandler) 135 136 go func() { 137 for { 138 select { 139 case tokenc <- true: 140 default: 141 } 142 time.Sleep(15 * time.Second) 143 } 144 }() 145 146 dir, err := osutil.GoPackagePath("camlistore.org") 147 if err != nil { 148 return err 149 } 150 151 hashes, err := recentCommits(dir) 152 if err != nil { 153 return err 154 } 155 for _, commit := range hashes { 156 knownCommit[commit] = true 157 } 158 latestHash.Lock() 159 latestHash.s = hashes[0] 160 latestHash.Unlock() 161 http.HandleFunc("/latesthash", latestHashHandler) 162 163 for { 164 pollCommits(dir) 165 166 // Poll every minute or whenever we're forced with the 167 // /mailnow handler. 168 select { 169 case <-time.After(1 * time.Minute): 170 case <-fetchc: 171 log.Printf("Polling git due to explicit trigger.") 172 } 173 } 174 } 175 176 func pollCommits(dir string) { 177 cmd := exec.Command("git", "fetch", "origin") 178 cmd.Dir = dir 179 out, err := cmd.CombinedOutput() 180 if err != nil { 181 log.Printf("Error running git fetch origin master in %s: %v\n%s", dir, err, out) 182 return 183 } 184 log.Printf("Ran git fetch.") 185 // TODO: see if .git/refs/remotes/origin/master changed. quicker. 186 187 hashes, err := recentCommits(dir) 188 if err != nil { 189 log.Print(err) 190 return 191 } 192 latestHash.Lock() 193 latestHash.s = hashes[0] 194 latestHash.Unlock() 195 for _, commit := range hashes { 196 if knownCommit[commit] { 197 continue 198 } 199 if err := emailCommit(dir, commit); err == nil { 200 knownCommit[commit] = true 201 log.Printf("Emailed commit %s", commit) 202 } 203 } 204 } 205 206 func recentCommits(dir string) (hashes []string, err error) { 207 cmd := exec.Command("git", "log", "--since=1 month ago", "--pretty=oneline", "origin/master") 208 cmd.Dir = dir 209 out, err := cmd.CombinedOutput() 210 if err != nil { 211 return nil, fmt.Errorf("Error running git log in %s: %v\n%s", dir, err, out) 212 } 213 for _, line := range strings.Split(string(out), "\n") { 214 v := strings.SplitN(line, " ", 2) 215 if len(v) > 1 { 216 hashes = append(hashes, v[0]) 217 } 218 } 219 return 220 } 221 222 func mailNowHandler(w http.ResponseWriter, r *http.Request) { 223 select { 224 case <-tokenc: 225 log.Printf("/mailnow got a token") 226 default: 227 // Too many requests. Ignore. 228 log.Printf("Ignoring /mailnow request; too soon.") 229 return 230 } 231 select { 232 case fetchc <- true: 233 log.Printf("/mailnow triggered a git fetch") 234 default: 235 } 236 } 237 238 func latestHashHandler(w http.ResponseWriter, r *http.Request) { 239 latestHash.Lock() 240 defer latestHash.Unlock() 241 fmt.Fprint(w, latestHash.s) 242 }