commit ea2b4e42b7004d4a6eeac61bc79a0e96414c54ff Author: gutmet Date: Sat Mar 26 19:13:36 2022 +0100 Initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5aa05eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.gitdist +vcf2fritzbox +go.sum diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..49abc83 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +vcf2fritzbox: Convert vCard phone numbers to FritzBox phone book format. +Copyright (C) 2022 Alexander Weinhold + +Unless explicitly stated otherwise, the following applies: + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 2 as published by +the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..47c3a0c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.gutmet.org/vcf2fritzbox.git + +go 1.16 diff --git a/vcf2fritzbox.go b/vcf2fritzbox.go new file mode 100644 index 0000000..30b53e4 --- /dev/null +++ b/vcf2fritzbox.go @@ -0,0 +1,217 @@ +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) +}