func DeleteUnusedTwilioNumber(db *periwinkle.Tx, num string) locale.Error { var twilioNum TwilioNumber if result := db.Where("number = ?", num).First(&twilioNum); result.Error != nil { if result.RecordNotFound() { periwinkle.Logf("The number is already deleted!!!") return nil } dbError(result.Error) return locale.UntranslatedError(result.Error) } var twilioPool TwilioPool result := db.Where("number_id = ?", twilioNum.ID).First(&twilioPool) if result.Error != nil { if result.RecordNotFound() { o := db.Where("number = ?", num).Delete(&TwilioNumber{}) if o.Error != nil { dbError(o.Error) return locale.UntranslatedError(o.Error) } periwinkle.Logf("The number is deleted") return nil } dbError(result.Error) return locale.UntranslatedError(result.Error) } periwinkle.Logf("The number is used for a twilio pool") return nil }
func checkNumber(config *periwinkle.Cfg, tx *periwinkle.Tx, number backend.TwilioNumber) { url := "https://api.twilio.com/2010-04-01/Accounts/" + config.TwilioAccountID + "/Messages.json?To=" + number.Number if lastPoll != timeZero { url += "&DateSent>=" + strings.Split(lastPoll.UTC().String(), " ")[0] } req, _ := http.NewRequest("GET", url, nil) req.SetBasicAuth(config.TwilioAccountID, config.TwilioAuthToken) resp, uerr := (&http.Client{}).Do(req) if uerr != nil { periwinkle.LogErr(locale.UntranslatedError(uerr)) } defer resp.Body.Close() body, uerr := ioutil.ReadAll(resp.Body) if uerr != nil { periwinkle.LogErr(locale.UntranslatedError(uerr)) } // converts JSON messages var page twilio.Paging json.Unmarshal([]byte(body), &page) for _, message := range page.Messages { timeSend, uerr := time.Parse(time.RFC1123Z, message.DateSent) if uerr != nil { periwinkle.LogErr(locale.UntranslatedError(uerr)) continue } if timeSend.Unix() < lastPoll.Unix() { periwinkle.Logf("message %q older than our last poll; ignoring", message.Sid) continue } user := backend.GetUserByAddress(tx, "sms", message.From) if user == nil { periwinkle.Logf("could not figure out which user has number %q", message.From) continue } group := backend.GetGroupByUserAndTwilioNumber(tx, user.ID, message.To) if group == nil { periwinkle.Logf("could not figure out which group this is meant for: user: %q, number: %q", user.ID, message.To) continue } periwinkle.Logf("received message for group %q", group.ID) MessageBuilder{ Maildir: config.Mailstore, Headers: map[string]string{ "To": group.ID + "@" + config.GroupDomain, "From": backend.UserAddress{Medium: "sms", Address: message.From}.AsEmailAddress(), "Subject": user.ID + ": " + message.Body, }, Body: "", }.Done() } }
func main() { options := cmdutil.Docopt(usage) config := cmdutil.GetConfig(options["-c"].(string)) var ret pp.ExitStatus = pp.EX_OK defer func() { if reason := recover(); reason != nil { const size = 64 << 10 buf := make([]byte, size) buf = buf[:runtime.Stack(buf, false)] st := fmt.Sprintf("%[1]T(%#[1]v) => %[1]v\n\n%[2]s", reason, string(buf)) periwinkle.Logf("%s", st) ret = pp.EX_UNAVAILABLE } pp.Exit(ret) }() conflict := config.DB.Do(func(transaction *periwinkle.Tx) { msg := pp.Get() recipient := msg.ORIGINAL_RECIPIENT() if recipient == "" { periwinkle.Logf("ORIGINAL_RECIPIENT must be set") ret = pp.EX_USAGE return } parts := strings.SplitN(recipient, "@", 2) user := parts[0] domain := "localhost" if len(parts) == 2 { domain = parts[1] } domain = strings.ToLower(domain) reader, err := msg.Reader() if err != nil { periwinkle.LogErr(err) ret = pp.EX_NOINPUT return } if handler, ok := config.DomainHandlers[domain]; ok { ret = handler(reader, user, transaction, config) } else { ret = config.DefaultDomainHandler(reader, recipient, transaction, config) } }) if conflict != nil { ret = pp.EX_DATAERR } }
func GetConfig(filename string) *periwinkle.Cfg { configFile, uerr := os.Open(filename) if uerr != nil { periwinkle.Logf("Could not open config file: %v", locale.UntranslatedError(uerr)) os.Exit(int(lsb.EXIT_NOTCONFIGURED)) } config, err := cfg.Parse(configFile) if err != nil { periwinkle.Logf("Could not parse config file: %v", err) os.Exit(int(lsb.EXIT_NOTCONFIGURED)) } return config }
func (server TwilioSMSCallbackServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { periwinkle.Logf("TwilioCallback") fmt.Fprintf(w, "Hi there, I love %s!", req.URL.String()) body, err := ioutil.ReadAll(req.Body) if err != nil { periwinkle.LogErr(locale.UntranslatedError(err)) return } values, err := url.ParseQuery(string(body)) if err != nil { periwinkle.LogErr(locale.UntranslatedError(err)) return } status := backend.TwilioSMSMessage{ MessageStatus: values.Get("MessageStatus"), ErrorCode: values.Get("ErrorCode"), MessageSID: values.Get("MessageSid"), } server.DB.Do(func(db *periwinkle.Tx) { status.Save(db) }) }
func GetAllExistingTwilioNumbers(cfg *periwinkle.Cfg) []string { // gets url for the numbers we own in the Twilio Account incomingNumURL := "https://api.twilio.com/2010-04-01/Accounts/" + cfg.TwilioAccountID + "/IncomingPhoneNumbers.json" client := &http.Client{} req, err := http.NewRequest("GET", incomingNumURL, nil) if err != nil { periwinkle.LogErr(locale.UntranslatedError(err)) return nil } req.SetBasicAuth(cfg.TwilioAccountID, cfg.TwilioAuthToken) resp, err := client.Do(req) if err != nil { periwinkle.LogErr(locale.UntranslatedError(err)) return nil } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { periwinkle.LogErr(locale.UntranslatedError(err)) return nil } if resp.StatusCode != 200 { periwinkle.Logf("Response code %s", resp.Status) return nil } numbers := IncomingNumbers{} if err := json.Unmarshal(body, &numbers); err != nil { periwinkle.LogErr(locale.UntranslatedError(err)) return nil } if len(numbers.PhoneNumbers) > 0 { existingNumbers := make([]string, len(numbers.PhoneNumbers)) for i, num := range numbers.PhoneNumbers { existingNumbers[i] = num.Number } return existingNumbers } else { return nil } }
func main() { options := cmdutil.Docopt(usage) args := []string{} if options["ADDR_TYPE"] != nil { args = append(args, options["ADDR_TYPE"].(string)) } if options["ADDR"] != nil { args = append(args, options["ADDR"].(string)) } socket := parseArgs(args) config := cmdutil.GetConfig(options["-c"].(string)) signals := make(chan os.Signal) signal.Notify(signals, syscall.SIGTERM, syscall.SIGHUP) periwinkle.Logf("Ready; listening") sd.Notify(false, "READY=1") done := make(chan uint8) server := httpapi.MakeServer(socket, config) server.Start() go func() { err := server.Wait() if err != nil { periwinkle.LogErr(err) done <- 1 } else { done <- 0 } }() for { select { case sig := <-signals: switch sig { case syscall.SIGTERM: sd.Notify(false, "STOPPING=1") server.Stop() case syscall.SIGHUP: sd.Notify(false, "RELOADING=1") // TODO: reload configuration file sd.Notify(false, "READY=1") } case status := <-done: os.Exit(int(status)) } } }
func main() { options := cmdutil.Docopt(usage) config := cmdutil.GetConfig(options["-c"].(string)) conflict := config.DB.Do(func(tx *periwinkle.Tx) { err := backend.DbSchema(tx) if err != nil { periwinkle.Logf("Encountered an error while setting up the database schema, not attempting to seed data:") periwinkle.LogErr(err) os.Exit(int(lsb.EXIT_FAILURE)) } err = backend.DbSeed(tx) if err != nil { periwinkle.Logf("Encountered an error while seeding the database:") periwinkle.LogErr(err) os.Exit(int(lsb.EXIT_FAILURE)) } }) if conflict != nil { periwinkle.LogErr(conflict) os.Exit(int(lsb.EXIT_FAILURE)) } }
func CreateTempDB() *periwinkle.Cfg { conf := periwinkle.Cfg{ Mailstore: "./Maildir", WebUIDir: "./www", Debug: true, TrustForwarded: true, GroupDomain: "localhost", WebRoot: "http://locahost:8080", DB: nil, // the default DB is set later } db, err := cfg.OpenDB("sqlite3", "file:temp.sqlite?mode=memory&_txlock=exclusive", false) if err != nil { periwinkle.Logf("Error loading sqlite3 database") } conf.DB = db conf.DB.Do(func(tx *periwinkle.Tx) { DbSchema(tx) }) return &conf }
func parseArgs(args []string) net.Listener { var stype, saddr string switch len(args) { case 0: stype = "tcp" saddr = ":8080" case 1: switch args[0] { case "tcp", "tcp4", "tcp6": stype = args[0] saddr = ":8080" case "unix": stype = args[0] saddr = "./http.sock" case "fd": stype = args[0] saddr = "stdin" case "systemd", "stdin", "stdout", "stderr": stype = "fd" saddr = args[0] default: if strings.ContainsRune(args[0], '/') { stype = "unix" } else if _, err := strconv.Atoi(args[0]); err == nil { stype = "fd" } else { stype = "tcp" } saddr = args[0] } case 2: stype = args[0] saddr = args[1] default: periwinkle.Logf(usage, os.Args[0]) os.Exit(int(lsb.EXIT_INVALIDARGUMENT)) } var socket net.Listener var err locale.Error if stype == "fd" { switch saddr { case "systemd": socket, err = sdGetSocket() case "stdin": socket, err = listenfd(0, "/dev/stdin") case "stdout": socket, err = listenfd(1, "/dev/stdout") case "stderr": socket, err = listenfd(2, "/dev/stderr") default: n, uerr := strconv.Atoi(saddr) if uerr == nil { socket, err = listenfd(n, "/dev/fd/"+saddr) } } } else { var uerr error socket, uerr = net.Listen(stype, saddr) err = locale.UntranslatedError(uerr) if tcpsock, ok := socket.(*net.TCPListener); ok { socket = tcpKeepAliveListener{tcpsock} } } if err != nil { periwinkle.LogErr(err) os.Exit(int(lsb.EXIT_FAILURE)) } return socket }
func Parse(in io.Reader) (cfgptr *periwinkle.Cfg, e locale.Error) { defer func() { if r := recover(); r != nil { cfgptr = nil switch err := r.(type) { case locale.Error: e = err default: panic(r) } } }() // these are the defaults hostname, err := os.Hostname() if err != nil { hostname = "localhost" } cfg := periwinkle.Cfg{ Mailstore: "./Maildir", WebUIDir: "./www", Debug: true, TrustForwarded: true, TwilioAccountID: os.Getenv("TWILIO_ACCOUNTID"), TwilioAuthToken: os.Getenv("TWILIO_TOKEN"), GroupDomain: "localhost", WebRoot: "http://" + hostname + ":8080", DB: nil, // the default DB is set later DefaultDomainHandler: bounceNoHost, } datstr, err := ioutil.ReadAll(in) if err != nil { gotoError(locale.UntranslatedError(err)) } var datint interface{} err = yaml.Unmarshal(datstr, &datint) if err != nil { gotoError(locale.UntranslatedError(err)) } datmap, ok := datint.(map[interface{}]interface{}) if !ok { gotoError(locale.Errorf("root element is not a map")) } var dbdriver string var dbsource string for key, val := range datmap { switch key { case "Mailstore": cfg.Mailstore = maildir.Maildir(getString(key.(string), val)) case "WebUIDir": cfg.WebUIDir = http.Dir(getString(key.(string), val)) case "Debug": cfg.Debug = getBool(key.(string), val) case "TrustForwarded": cfg.TrustForwarded = getBool(key.(string), val) case "TwilioAccountID": cfg.TwilioAccountID = getString(key.(string), val) case "TwilioAuthToken": cfg.TwilioAuthToken = getString(key.(string), val) case "GroupDomain": cfg.GroupDomain = getString(key.(string), val) case "WebRoot": cfg.WebRoot = getString(key.(string), val) case "DB": m, ok := val.(map[interface{}]interface{}) if !ok { gotoError(locale.Errorf("value for %q is not a map", key.(string))) } for key, val := range m { switch key { case "driver": dbdriver = getString("DB."+key.(string), val) case "source": dbsource = getString("DB."+key.(string), val) default: gotoError(locale.Errorf("unknown field: %v", "DB."+key.(string))) } } default: gotoError(locale.Errorf("unknown field: %v", key)) } } if dbdriver != "" && dbsource != "" { db, err := OpenDB(dbdriver, dbsource, cfg.Debug) if err != nil { gotoError(err) } cfg.DB = db } // Set the default database if cfg.DB == nil { periwinkle.Logf("DB not configured, trying MySQL periwinkle:periwinkle@localhost/periwinkle ...") db, err := OpenDB("mysql", "periwinkle:periwinkle@/periwinkle?charset=utf8&parseTime=True", cfg.Debug) if err != nil { periwinkle.Logf("Could not connect to MySQL: %v", locale.UntranslatedError(err)) periwinkle.Logf("No MySQL, trying SQLite3 file:periwinkle.sqlite ...") db, err = OpenDB("sqlite3", "file:periwinkle.sqlite?mode=rwc&_txlock=exclusive", cfg.Debug) if err != nil { periwinkle.Logf("Could not open SQLite3 DB: %v", locale.UntranslatedError(err)) gotoError(locale.Errorf("Could not connect to database")) } } cfg.DB = db } domain_handlers.GetHandlers(&cfg) return &cfg, nil }
func HandleEmail(r io.Reader, name string, db *periwinkle.Tx, cfg *periwinkle.Cfg) postfixpipe.ExitStatus { mdWriter := cfg.Mailstore.NewMail() if mdWriter == nil { periwinkle.Logf("Could not open maildir for writing: %q\n", cfg.Mailstore) return postfixpipe.EX_IOERR } // As we read the message, also write it to the maildir defer func() { if mdWriter != nil { mdWriter.Cancel() } }() r = io.TeeReader(r, mdWriter) // Read the message msg, err := mail.ReadMessage(r) if err != nil { return postfixpipe.EX_NOINPUT } // Figure out which group it was to group := backend.GetGroupByID(db, name) if group == nil { return postfixpipe.EX_NOUSER } // Figure out who sent it //user_email := msg.Header.Get("From") //user := backend.GetUserByAddress(db, "email", user_email) // check permissions //if user == nil || !CanPost(db, group, user.ID) { // return postfixpipe.EX_NOPERM //} // Add it to the database backend.NewMessage( db, msg.Header.Get("Message-Id"), *group, mdWriter.Unique()) mdWriter.Close() mdWriter = nil // Generate the list of who we're sending it to var forwardAry []string { // collect IDs of addresses subscribed to the group addressIDs := make([]int64, len(group.Subscriptions)) for i := range group.Subscriptions { addressIDs[i] = group.Subscriptions[i].AddressID } // fetch all of those addresses var addressList []backend.UserAddress if len(addressIDs) > 0 { db.Where("id IN (?)", addressIDs).Find(&addressList) } else { addressList = make([]backend.UserAddress, 0) } // convert that list into a set forwardSet := make(map[string]bool, len(addressList)) for _, addr := range addressList { if addr.Medium != "noop" && addr.Medium != "admin" { forwardSet[addr.AsEmailAddress()] = true } } // prune addresses that (should) already have the message for _, header := range []string{"To", "From", "Cc"} { addresses, err := msg.Header.AddressList(header) if err != nil { periwinkle.Logf("Parsing %q Header: %v\n", header, err) } for _, addr := range addresses { delete(forwardSet, addr.Address) } } // TODO: also prune addresses that belong to user. // convert the set into an array forwardAry = make([]string, len(forwardSet)) i := uint(0) for addr := range forwardSet { forwardAry[i] = addr i++ } } periwinkle.Logf("Forwarding message to group %q to user addresses %#v", group.ID, forwardAry) // format the message msg822 := []byte{} for k := range msg.Header { msg822 = append(msg822, []byte(fmt.Sprintf("%s: %s\r\n", k, msg.Header.Get(k)))...) } msg822 = append(msg822, []byte("\r\n")...) body, _ := ioutil.ReadAll(msg.Body) // TODO: error handling msg822 = append(msg822, body...) if len(forwardAry) > 0 { // send the message out err = smtp.SendMail("localhost:25", smtp.PlainAuth("", "", "", ""), msg.Header.Get("From"), forwardAry, msg822) if err != nil { periwinkle.Logf("Error sending: %v", err) return postfixpipe.EX_UNAVAILABLE } } return postfixpipe.EX_OK }