perkeep/website/email.go

243 lines
5.3 KiB
Go

/*
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"flag"
"fmt"
"log"
"net/http"
"net/smtp"
"os"
"os/exec"
"strings"
"sync"
"time"
"camlistore.org/pkg/osutil"
)
var (
emailNow = flag.String("email_now", "", "If non-empty, this commit hash is emailed immediately, without starting the webserver.")
smtpServer = flag.String("smtp_server", "127.0.0.1:25", "SMTP server")
emailsTo = flag.String("email_dest", "", "If non-empty, the email address to email commit emails.")
)
func startEmailCommitLoop(errc chan<- error) {
if *emailsTo == "" {
return
}
if *emailNow != "" {
dir, err := osutil.GoPackagePath("camlistore.org")
if err != nil {
log.Fatal(err)
}
if err := emailCommit(dir, *emailNow); err != nil {
log.Fatal(err)
}
os.Exit(0)
}
go func() {
errc <- commitEmailLoop()
}()
}
// tokenc holds tokens for the /mailnow handler.
// Hitting /mailnow (unauthenticated) forces a 'git fetch origin
// master'. Because it's unauthenticated, we don't want to allow
// attackers to force us to hit git. The /mailnow handler tries to
// take a token from tokenc.
var tokenc = make(chan bool, 3)
var fetchc = make(chan bool, 1)
var knownCommit = map[string]bool{} // commit -> true
var diffMarker = []byte("diff --git a/")
func emailCommit(dir, hash string) (err error) {
cmd := exec.Command("git", "show", hash)
cmd.Dir = dir
body, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("Error runnning git show: %v\n%s", err, body)
}
if !bytes.Contains(body, diffMarker) {
// Boring merge commit. Don't email.
return nil
}
cmd = exec.Command("git", "show", "--pretty=oneline", hash)
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return
}
subj := out[41:] // remove hash and space
if i := bytes.IndexByte(subj, '\n'); i != -1 {
subj = subj[:i]
}
if len(subj) > 80 {
subj = subj[:80]
}
cl, err := smtp.Dial(*smtpServer)
if err != nil {
return
}
defer cl.Quit()
if err = cl.Mail("noreply@camlistore.org"); err != nil {
return
}
if err = cl.Rcpt(*emailsTo); err != nil {
return
}
wc, err := cl.Data()
if err != nil {
return
}
_, err = fmt.Fprintf(wc, `From: noreply@camlistore.org (Camlistore Commit)
To: %s
Subject: %s
Reply-To: camlistore@googlegroups.com
https://camlistore.googlesource.com/camlistore/+/%s
%s`, *emailsTo, subj, hash, body)
if err != nil {
return
}
return wc.Close()
}
var latestHash struct {
sync.Mutex
s string // hash of the most recent camlistore revision
}
func commitEmailLoop() error {
http.HandleFunc("/mailnow", mailNowHandler)
go func() {
for {
select {
case tokenc <- true:
default:
}
time.Sleep(15 * time.Second)
}
}()
dir, err := osutil.GoPackagePath("camlistore.org")
if err != nil {
return err
}
hashes, err := recentCommits(dir)
if err != nil {
return err
}
for _, commit := range hashes {
knownCommit[commit] = true
}
latestHash.Lock()
latestHash.s = hashes[0]
latestHash.Unlock()
http.HandleFunc("/latesthash", latestHashHandler)
for {
pollCommits(dir)
// Poll every minute or whenever we're forced with the
// /mailnow handler.
select {
case <-time.After(1 * time.Minute):
case <-fetchc:
log.Printf("Polling git due to explicit trigger.")
}
}
}
func pollCommits(dir string) {
cmd := exec.Command("git", "fetch", "origin")
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Error running git fetch origin master in %s: %v\n%s", dir, err, out)
return
}
log.Printf("Ran git fetch.")
// TODO: see if .git/refs/remotes/origin/master changed. quicker.
hashes, err := recentCommits(dir)
if err != nil {
log.Print(err)
return
}
latestHash.Lock()
latestHash.s = hashes[0]
latestHash.Unlock()
for _, commit := range hashes {
if knownCommit[commit] {
continue
}
if err := emailCommit(dir, commit); err == nil {
knownCommit[commit] = true
log.Printf("Emailed commit %s", commit)
}
}
}
func recentCommits(dir string) (hashes []string, err error) {
cmd := exec.Command("git", "log", "--since=1 month ago", "--pretty=oneline", "origin/master")
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("Error running git log in %s: %v\n%s", dir, err, out)
}
for _, line := range strings.Split(string(out), "\n") {
v := strings.SplitN(line, " ", 2)
if len(v) > 1 {
hashes = append(hashes, v[0])
}
}
return
}
func mailNowHandler(w http.ResponseWriter, r *http.Request) {
select {
case <-tokenc:
log.Printf("/mailnow got a token")
default:
// Too many requests. Ignore.
log.Printf("Ignoring /mailnow request; too soon.")
return
}
select {
case fetchc <- true:
log.Printf("/mailnow triggered a git fetch")
default:
}
}
func latestHashHandler(w http.ResponseWriter, r *http.Request) {
latestHash.Lock()
defer latestHash.Unlock()
fmt.Fprint(w, latestHash.s)
}