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)
}