package main import ( "encoding/xml" "fmt" "io" "io/ioutil" "mime/quotedprintable" "os" "strings" "time" ) func complain(s string) { fmt.Fprintln(os.Stderr, s) } func optExit(err error) { if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(-1) } } // the vCard RFC says, a single logical line can be "folded" to multiple physical lines, // so this shit must be done type UnfoldingReader struct { buf []byte i int j int } func NewUnfoldingReader(buf []byte) *UnfoldingReader { return &UnfoldingReader{buf, 0, 0} } func replaceFold(s string) string { // Yes, I know and I don't care return strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(s, "\r\n ", ""), "\r\n\t", "")) } func (r *UnfoldingReader) ReadLine() (string, bool) { readCR := false readCRLF := false for r.j = r.i; r.j < len(r.buf); r.j++ { if r.buf[r.j] == '\r' { readCR = true } else if readCR && r.buf[r.j] == '\n' { readCRLF = true } else if readCRLF && r.buf[r.j] != ' ' && r.buf[r.j] != '\t' { break } else { readCRLF = false } } var s string if r.i < len(r.buf) { s = replaceFold(string(r.buf[r.i:r.j])) } else { s = "" } notEOB := (r.i < len(r.buf)) r.i = r.j return s, notEOB } func vCardValue(line string) string { start := strings.LastIndex(line, ":") end := strings.LastIndex(line, ";") if start == -1 { return "" } if end < start { end = len(line) - 1 } return line[start+1 : end+1] } type Contact struct { Name string Numbers NumberSet } type ContactSet []Contact var uniqueID int = 1741 const xmlPreamble = ` AVM Ansage (HD)500@hd-telefonie.avm.de0HD-Musik200@hd-telefonie.avm.de1HD-Sprache100@hd-telefonie.avm.de2 ` const xmlEnd = ` ` func (c ContactSet) ToFritzboxXML() string { var s strings.Builder s.WriteString(xmlPreamble) for _, contact := range c { contact.WriteFritzboxXML(&s) } s.WriteString(xmlEnd) return s.String() } func (c *Contact) WriteFritzboxXML(s io.Writer) { if c == nil { return } uniqueID++ fmt.Fprint(s, "0") xml.Escape(s, []byte(c.Name)) fmt.Fprintf(s, "\n", len(c.Numbers)) c.Numbers.WriteFritzboxXML(s) fmt.Fprintf(s, "%d\n\n", uniqueID) } func (c *Contact) setName(line string) { if c == nil { c = &Contact{} } c.Name = vCardValue(line) if strings.Count(c.Name, "=") > 1 { tmp, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(c.Name))) if err == nil { c.Name = string(tmp) } } } func (c *Contact) addNumber(line string) { if c == nil { c = &Contact{} } tolower := strings.ToLower(line) val := vCardValue(line) var n Number if strings.Contains(tolower, "cell") { n = Number{val, "mobile"} } else { n = Number{val, "home"} } c.Numbers = append(c.Numbers, n) } func (c *Contact) setValue(line string) { if strings.HasPrefix(line, "FN") { c.setName(line) } else if strings.HasPrefix(line, "TEL") { c.addNumber(line) } } type Number struct { Value string Kind string } type NumberSet []Number func (n NumberSet) WriteFritzboxXML(s io.Writer) { for i, number := range n { number.WriteFritzboxXML(s, i) } } func (n *Number) WriteFritzboxXML(s io.Writer, i int) { if n == nil { return } var prio int if i == 0 { prio = 1 } else { prio = 0 } fmt.Fprintf(s, `%s`, n.Kind, prio, i, n.Value) fmt.Fprint(s, "\n") } func GetContacts(buf []byte) (c ContactSet) { r := NewUnfoldingReader(buf) var current *Contact for l, ok := r.ReadLine(); ok; l, ok = r.ReadLine() { if l == "BEGIN:VCARD" { current = &Contact{} } else if l == "END:VCARD" { c = append(c, *current) current = nil } else { current.setValue(l) } } if current != nil { c = append(c, *current) } return } func main() { if len(os.Args) != 2 { fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "VCF-FILE") os.Exit(-1) } b, err := ioutil.ReadFile(os.Args[1]) optExit(err) outfile := os.Args[1] + "." + time.Now().Format("20060102-150405") + ".xml" err = ioutil.WriteFile(outfile, []byte(GetContacts(b).ToFritzboxXML()), 0644) optExit(err) fmt.Println(outfile) }