Initial
This commit is contained in:
commit
ea2b4e42b7
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.gitdist
|
||||||
|
vcf2fritzbox
|
||||||
|
go.sum
|
16
LICENSE
Normal file
16
LICENSE
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
217
vcf2fritzbox.go
Normal file
217
vcf2fritzbox.go
Normal file
|
@ -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 = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<phonebooks>
|
||||||
|
<phonebook>
|
||||||
|
<contact><category /><person><realName>AVM Ansage (HD)</realName></person><telephony
|
||||||
|
nid="1"><number prio="1" type="work" quickdial="99" id="0">500@hd-telefonie.avm.de</number></telephony><services /><setup /><uniqueid>0</uniqueid></contact><contact><category /><person><realName>HD-Musik</realName></person><telephony
|
||||||
|
nid="1"><number prio="1" type="home" quickdial="97" id="0">200@hd-telefonie.avm.de</number></telephony><services /><setup /><uniqueid>1</uniqueid></contact><contact><category /><person><realName>HD-Sprache</realName></person><telephony
|
||||||
|
nid="1"><number prio="1" type="home" quickdial="98" id="0">100@hd-telefonie.avm.de</number></telephony><services /><setup /><uniqueid>2</uniqueid></contact>
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
const xmlEnd = `</phonebook>
|
||||||
|
</phonebooks>
|
||||||
|
`
|
||||||
|
|
||||||
|
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, "<contact><category>0</category><person><realName>")
|
||||||
|
xml.Escape(s, []byte(c.Name))
|
||||||
|
fmt.Fprintf(s, "</realName></person><telephony nid=\"%d\">\n", len(c.Numbers))
|
||||||
|
c.Numbers.WriteFritzboxXML(s)
|
||||||
|
fmt.Fprintf(s, "</telephony><services /><setup /><uniqueid>%d</uniqueid></contact>\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, `<number type="%s" prio="%d" id="%d">%s</number>`, 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user