Source

CitadelSync / sync.go

Full commit
// Import vCards to the Citadel Mail Server
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"strings"
	"time"

	"bitbucket.org/gotamer/cfg"
	"bitbucket.org/gotamer/citadel"
	"bitbucket.org/gotamer/errors"
	"bitbucket.org/gotamer/tools"
)

const (
	VERSION      = 2
	VCARD_TIME   = "20060102T150405Z0700"
	DEFAULT_PORT = ":504"
)

var (
	name     = flag.String("n", "", "Name of the configuration contacts, notes, calender etc.")
	helpflag = flag.Bool("h", false, "Prints out this help text")
	username = flag.String("u", "", "Citadel Username")
	password = flag.String("p", "", "Citadel Password")
	purge    = flag.Bool("D", false, "Delete all items in the room!")
	version  = flag.Bool("v", false, "version")
)

var helpTop = `
  ****************************************
               Citadel Go Sync
  ****************************************

  Upload files to a Citadel Mail Server

 - Contacts from vCards ".vcf"
 - Notes from vNotes ".vnt"
 - Calendar from vCalendars ".vcs" or ".ics"
 - Task from vCalendars ".vcs" or ".ics"
 - Text from any text based files ".txt"

`

var helpButtom = `

  -n Based on the name flag a configuration template file will be created

  -u and -p / Username and Password for the Citadel Mail Server may be
  defined in the config file, or optionaly on the command line

  -D will delete all items in the given room WITHOUT WARNING

`

var (
	PS       string = string(os.PathSeparator)
	Cfg      *config
	FILE_DB  string
	FILE_CFG string
	FILE_LOG string
	DB       map[string]*database
)

type config struct {
	Version     int8
	Environment e.Env
	LocalDir    string
	Room        string
	Username    string
	Password    string
	Server      string
	Port        string
	Floor       string
	SSL_KEY     string
	SSL_CER     string
	citRoomType string
}

func init() {
	Cfg = new(config)
	Cfg.Version = VERSION
	Cfg.Environment = e.ENV_PROD
	Cfg.Username = "TaMeR"
	Cfg.Password = "God knows what"
	Cfg.Server = "localhost"
	Cfg.Port = DEFAULT_PORT
	Cfg.Room = "Contacts"
	Cfg.LocalDir = os.TempDir() + PS + "vcard"
	Cfg.Floor = "Not implemented"
	Cfg.SSL_CER = "Not implemented"
	Cfg.SSL_KEY = "Not implemented"

	DB = make(map[string]*database)
}

func main() {
	flag.Parse()
	if *version {
		fmt.Printf("\n\tCitadel Import Version %v\n\n", VERSION)
		os.Exit(0)
	}
	if *helpflag || *name == "" {
		fmt.Println(helpTop)
		flag.PrintDefaults()
		fmt.Println(helpButtom)
		os.Exit(0)
	}
	FILE_DB = fmt.Sprintf("%s.db.json", *name)
	FILE_CFG = fmt.Sprintf("%s.cfg.json", *name)
	FILE_LOG = fmt.Sprintf("%s.log", *name)

	if err := cfg.Load(FILE_CFG, Cfg); err != nil {
		Cfg.LocalDir = os.Getenv("HOME") + PS + "PIM" + PS + *name
		if err = cfg.Save(FILE_CFG, Cfg); err != nil {
			fmt.Println("cfg.Save error: ", err.Error())
			os.Exit(1)
		} else {
			fmt.Printf("\nPlease edit your config file at:\n\n\t%s\n", FILE_CFG)
			os.Exit(0)
		}
	}

	if Cfg.Version != VERSION {
		fmt.Println("Please check and upgrade your config file version to the new version: ", VERSION)
		os.Exit(0)
	}

	LogFile()

	if len(*username) != 0 {
		Cfg.Username = *username
	}

	if len(*password) != 0 {
		Cfg.Password = *password
	}

	if *purge {
		CitadelDeleteAll()
		os.Exit(0)
	}

	if !checkRoom() {
		os.Exit(0)
	}

	if Cfg.Server != "" {
		FilesInfo()
		//fmt.Println(DB)
		CitadelSend()
	}
}

type database struct {
	CitEUID      string
	FileName     string
	FileMimeType string
	FileModTime  time.Time
	fileModified bool
}

func (db *database) Set() {
	DB[db.FileName] = db
}

func (db *database) Del() {
	delete(DB, db.FileName)
}

func (db *database) Modified(fi os.FileInfo) bool {
	if db.FileName != "" {
		if db.FileModTime != fi.ModTime() {
			db.fileModified = true
		}
	} else {
		db.FileModTime = fi.ModTime()
		db.FileName = fi.Name()
		db.fileModified = true
		DB[db.FileName] = db
	}
	return db.fileModified
}

// TODO use linux "file -bip"
func (db *database) MimeType() (ok bool) {
	if db.FileName != "" {
		suffix := Suffix(db.FileName)
		switch suffix {
		case "vcf":
			db.FileMimeType = "Content-type: text/x-vcard"
			ok = true
			if Cfg.citRoomType != citadel.VIEW_ADDRESSBOOK {
				e.Info("WARNING not a vCard Room. Code is: %v", Cfg.citRoomType)
			}
		case "vnt":
			db.FileMimeType = "Content-type: text/vnote"
			ok = true
			if Cfg.citRoomType != citadel.VIEW_NOTES {
				e.Info("WARNING not a vNote Room. Code is: %v", Cfg.citRoomType)
			}
		case "vcs", "ics":
			db.FileMimeType = "Content-type: text/calendar"
			ok = true
			if Cfg.citRoomType != citadel.VIEW_CALENDAR {
				e.Info("WARNING not a Calender Room. Code is: %v", Cfg.citRoomType)
			}
			if Cfg.citRoomType != citadel.VIEW_TASKS {
				e.Info("WARNING not a Task Room. Code is: %v", Cfg.citRoomType)
			}
		case "txt":
			db.FileMimeType = "Content-type: text/text"
			ok = true
			if Cfg.citRoomType != citadel.VIEW_MAILBOX || Cfg.citRoomType != citadel.VIEW_BBS {
				e.Info("WARNING not a text Room. Code is: %v", Cfg.citRoomType)
			}
		default:
			db.FileMimeType = "Content-type: text/text"
			e.Info("WARNING Can't determen file type, using text!")
		}
	}
	return
}

func (db *database) CitadelEUID() {
	if db.FileName != "" {
		if db.CitEUID == "" {
			db.CitEUID = tools.Uid16()
			db.fileModified = true
		}
	}
}

func FilesInfo() {
	fis := readDir(Cfg.LocalDir)
	no := len(fis)
	for i := 0; i < no; i++ {
		if fis[i].IsDir() {
			continue
		}
		dbitem, ok := DB[fis[i].Name()]
		if !ok {
			dbitem = new(database)
		}

		dbitem.Modified(fis[i])
		dbitem.MimeType()
		dbitem.CitadelEUID()
	}
}

func readDir(path string) (fis []os.FileInfo) {
	fi, err := os.Stat(path)
	e.Check(err)
	if fi.IsDir() {
		f, err := os.Open(path)
		defer f.Close()
		e.Check(err)
		fis, err = f.Readdir(0) // 0 = All
		e.Check(err)
	}
	return
}

func CitadelSend() {
	c := citadel.New(Cfg.Server + Cfg.Port)
	defer c.Close()
	c.Login(Cfg.Username, Cfg.Password)
	c.Goto(Cfg.Room)
	c.Info()

	for _, dbitem := range DB {
		if dbitem.fileModified == false {
			e.Info("Not modified, not sending!")
			continue
		}

		bytes, err := ioutil.ReadFile(Cfg.LocalDir + PS + dbitem.FileName)
		ok := e.Check(err)
		if ok {
			cmd := fmt.Sprintf("ENT0 1|||4||||||%s", dbitem.CitEUID)
			c.Request(cmd)
			if c.Code == citadel.CODE_SEND_LISTING {
				c.Error = c.Conn.PrintfLine(dbitem.FileMimeType)
				c.Check()
				err = c.Conn.PrintfLine("%s", "\n")
				e.Check(err)
				err = c.Conn.PrintfLine("%s", bytes)
				e.Check(err)
				err = c.Conn.PrintfLine("%s", citadel.DE)
				e.Check(err)
				e.Info("%s", bytes)
			}
		}
	}
}

func CitadelDeleteAll() {
	c := citadel.New(Cfg.Server + Cfg.Port)
	defer c.Close()
	c.Login(Cfg.Username, Cfg.Password)
	c.Goto(Cfg.Room)
	c.Info()
	list, ok := c.MsgListAll()
	if ok {
		c.MsgsDel(list)
	}
}

func checkRoom() (ok bool) {
	c := citadel.New(Cfg.Server + Cfg.Port)
	defer c.Close()
	c.Login(Cfg.Username, Cfg.Password)
	if ok = c.Goto(Cfg.Room); !ok {
		e.Info("Room not availabe")
		c.Info()
	} else {
		Cfg.citRoomType = c.Resp[12]
	}
	return
}

func DBLoad() {
	if err := cfg.Load(FILE_DB, &DB); err != nil {
		e.Info("Will create new database at: %v", FILE_DB)
	}
}

func DBSave() {
	if err := cfg.Save(FILE_DB, DB); err != nil {
		e.Info("Could not create database at: ", FILE_DB)
		fmt.Println("Could not create database at: %v", FILE_DB)
	}
}

func LogFile() {
	e.ENVIRONMENT = Cfg.Environment
	if e.ENVIRONMENT != e.ENV_FAIL {
		f, err := os.OpenFile(FILE_LOG, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0664)
		e.Check(err)
		e.Logger(f)
	}
}

func Suffix(filename string) (suffix string) {
	no := strings.LastIndex(filename, ".")
	if no > 0 {
		suffix = filename[no+1:]
	}
	return
}