예제 #1
0
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
}
예제 #2
0
파일: main.go 프로젝트: LukeShu/periwinkle
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()
	}
}
예제 #3
0
파일: main.go 프로젝트: LukeShu/periwinkle
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
	}
}
예제 #4
0
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
}
예제 #5
0
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)
	})
}
예제 #6
0
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
	}
}
예제 #7
0
파일: main.go 프로젝트: LukeShu/periwinkle
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))
		}
	}
}
예제 #8
0
파일: main.go 프로젝트: LukeShu/periwinkle
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))
	}
}
예제 #9
0
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
}
예제 #10
0
파일: main.go 프로젝트: LukeShu/periwinkle
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
}
예제 #11
0
파일: parse.go 프로젝트: LukeShu/periwinkle
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
}
예제 #12
0
파일: email.go 프로젝트: LukeShu/periwinkle
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
}