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  }