diff --git a/LICENSE b/LICENSE
index d35547a..5ec274c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2015, Mickaël Rémond
+Copyright (c) 2016, ProcessOne
All rights reserved.
Redistribution and use in source and binary forms, with or without
diff --git a/xmpp/auth.go b/xmpp/auth.go
new file mode 100644
index 0000000..711e077
--- /dev/null
+++ b/xmpp/auth.go
@@ -0,0 +1,79 @@
+package xmpp
+
+import (
+ "encoding/base64"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+)
+
+func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f streamFeatures, user string, password string) (err error) {
+ // TODO: Implement other type of SASL Authentication
+ havePlain := false
+ for _, m := range f.Mechanisms.Mechanism {
+ if m == "PLAIN" {
+ havePlain = true
+ break
+ }
+ }
+ if !havePlain {
+ return errors.New(fmt.Sprintf("PLAIN authentication is not supported by server: %v", f.Mechanisms.Mechanism))
+ }
+
+ return authPlain(socket, decoder, user, password)
+}
+
+// Plain authentication: send base64-encoded \x00 user \x00 password
+func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password string) error {
+ raw := "\x00" + user + "\x00" + password
+ enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
+ base64.StdEncoding.Encode(enc, []byte(raw))
+ fmt.Fprintf(socket, "%s", nsSASL, enc)
+
+ // Next message should be either success or failure.
+ name, val, err := next(decoder)
+ if err != nil {
+ return err
+ }
+
+ switch v := val.(type) {
+ case *saslSuccess:
+ case *saslFailure:
+ // v.Any is type of sub-element in failure, which gives a description of what failed.
+ return errors.New("auth failure: " + v.Any.Local)
+ default:
+ return errors.New("expected success or failure, got " + name.Local + " in " + name.Space)
+ }
+ return err
+}
+
+// XMPP Packet Parsing
+type saslMechanisms struct {
+ XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
+ Mechanism []string `xml:"mechanism"`
+}
+
+type saslSuccess struct {
+ XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
+}
+
+type saslFailure struct {
+ XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
+ Any xml.Name // error reason is a subelement
+
+}
+
+type bindBind struct {
+ XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
+ Resource string
+ Jid string
+}
+
+// Session is obsolete in RFC 6121.
+// Added for compliance with RFC 3121.
+// Remove when ejabberd purely conforms to RFC 6121.
+type sessionSession struct {
+ XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
+ optional xml.Name // If it does exist, it mean we are not required to open session
+}
diff --git a/xmpp/client.go b/xmpp/client.go
new file mode 100644
index 0000000..0b8dc68
--- /dev/null
+++ b/xmpp/client.go
@@ -0,0 +1,116 @@
+package xmpp
+
+import (
+ "bytes"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "net"
+ "strings"
+)
+
+type Client struct {
+ // Store user defined options
+ options Options
+ // Session gather data that can be accessed by users of this library
+ Session *Session
+ // TCP level connection / can be replace by a TLS session after starttls
+ conn net.Conn
+}
+
+/*
+Setting up the client / Checking the parameters
+*/
+
+// TODO: better options check
+func NewClient(options Options) (c *Client, err error) {
+ // TODO: If option address is nil, use the Jid domain to compose the address
+ if options.Address, err = checkAddress(options.Address); err != nil {
+ return
+ }
+
+ if options.Password == "" {
+ err = errors.New("missing password")
+ return
+ }
+
+ c = new(Client)
+ c.options = options
+
+ // Parse JID
+ if c.options.parsedJid, err = NewJid(c.options.Jid); err != nil {
+ return
+ }
+
+ return
+}
+
+func checkAddress(addr string) (string, error) {
+ var err error
+ hostport := strings.Split(addr, ":")
+ if len(hostport) > 2 {
+ err = errors.New("too many colons in xmpp server address")
+ return addr, err
+ }
+
+ // Address is composed of two parts, we are good
+ if len(hostport) == 2 && hostport[1] != "" {
+ return addr, err
+ }
+
+ // Port was not passed, we append XMPP default port:
+ return strings.Join([]string{hostport[0], "5222"}, ":"), err
+}
+
+// NewClient creates a new connection to a host given as "hostname" or "hostname:port".
+// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
+// Default the port to 5222.
+func (c *Client) Connect() (*Session, error) {
+ var tcpconn net.Conn
+ var err error
+ if tcpconn, err = net.Dial("tcp", c.options.Address); err != nil {
+ return nil, err
+ }
+
+ c.conn = tcpconn
+ if c.conn, c.Session, err = NewSession(c.conn, c.options); err != nil {
+ return c.Session, err
+ }
+
+ // We're connected and can now receive and send messages.
+ //fmt.Fprintf(client.conn, "%s%s", "chat", "Online")
+ fmt.Fprintf(c.Session.socketProxy, "")
+
+ return c.Session, err
+}
+
+func (c *Client) recv(receiver chan<- interface{}) (err error) {
+ for {
+ _, val, err := next(c.Session.decoder)
+ if err != nil {
+ return err
+ }
+ receiver <- val
+ val = nil
+ }
+ panic("unreachable")
+}
+
+// Channel allow client to receive / dispatch packets in for range loop
+func (c *Client) Recv() <-chan interface{} {
+ ch := make(chan interface{})
+ go c.recv(ch)
+ return ch
+}
+
+// Send sends message text.
+func (c *Client) Send(packet string) error {
+ fmt.Fprintf(c.Session.socketProxy, packet)
+ return nil
+}
+
+func xmlEscape(s string) string {
+ var b bytes.Buffer
+ xml.Escape(&b, []byte(s))
+ return b.String()
+}
diff --git a/xmpp/iq.go b/xmpp/iq.go
new file mode 100644
index 0000000..2e97d8f
--- /dev/null
+++ b/xmpp/iq.go
@@ -0,0 +1,11 @@
+package xmpp
+
+import "encoding/xml"
+
+type clientIQ struct { // info/query
+ XMLName xml.Name `xml:"jabber:client iq"`
+ Packet
+ Bind bindBind
+ // TODO We need to support detecting the IQ namespace / Query packet
+ // Error clientError
+}
diff --git a/xmpp/jid.go b/xmpp/jid.go
new file mode 100644
index 0000000..6ff9364
--- /dev/null
+++ b/xmpp/jid.go
@@ -0,0 +1,34 @@
+package xmpp
+
+import (
+ "errors"
+ "strings"
+)
+
+type Jid struct {
+ username string
+ domain string
+ resource string
+}
+
+func NewJid(sjid string) (jid *Jid, err error) {
+ s1 := strings.Split(sjid, "@")
+ if len(s1) != 2 {
+ err = errors.New("invalid JID: " + sjid)
+ return
+ }
+ jid = new(Jid)
+ jid.username = s1[0]
+
+ s2 := strings.Split(s1[1], "/")
+ if len(s2) > 2 {
+ err = errors.New("invalid JID: " + sjid)
+ return
+ }
+ jid.domain = s2[0]
+ if len(s2) == 2 {
+ jid.resource = s2[1]
+ }
+
+ return
+}
diff --git a/xmpp/message.go b/xmpp/message.go
new file mode 100644
index 0000000..813d9c0
--- /dev/null
+++ b/xmpp/message.go
@@ -0,0 +1,24 @@
+package xmpp
+
+import (
+ "encoding/xml"
+ "fmt"
+)
+
+// XMPP Packet Parsing
+type ClientMessage struct {
+ XMLName xml.Name `xml:"jabber:client message"`
+ Packet
+ Subject string `xml:"subject,omitempty"`
+ Body string `xml:"body,omitempty"`
+ Thread string `xml:"thread,omitempty"`
+}
+
+// TODO: Func new message to create an empty message structure without the XML tag matching elements
+
+func (message *ClientMessage) XMPPFormat() string {
+ return fmt.Sprintf(""+
+ "%s",
+ message.To,
+ xmlEscape(message.Body))
+}
diff --git a/xmpp/ns.go b/xmpp/ns.go
new file mode 100644
index 0000000..cf0f5fe
--- /dev/null
+++ b/xmpp/ns.go
@@ -0,0 +1,10 @@
+package xmpp
+
+const (
+ nsStream = "http://etherx.jabber.org/streams"
+ nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
+ nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
+ nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
+ nsSession = "urn:ietf:params:xml:ns:xmpp-session"
+ nsClient = "jabber:client"
+)
diff --git a/xmpp/options.go b/xmpp/options.go
new file mode 100644
index 0000000..96bafe6
--- /dev/null
+++ b/xmpp/options.go
@@ -0,0 +1,12 @@
+package xmpp
+
+import "os"
+
+type Options struct {
+ Address string
+ Jid string
+ parsedJid *Jid // For easier manipulation
+ Password string
+ PacketLogger *os.File // Used for debugging
+ Lang string // TODO: should default to 'en'
+}
diff --git a/xmpp/packet.go b/xmpp/packet.go
new file mode 100644
index 0000000..72098c1
--- /dev/null
+++ b/xmpp/packet.go
@@ -0,0 +1,13 @@
+package xmpp
+
+type Packet struct {
+ Id string `xml:"id,attr,omitempty"`
+ From string `xml:"from,attr,omitempty"`
+ To string `xml:"to,attr,omitempty"`
+ Type string `xml:"type,attr,omitempty"`
+ Lang string `xml:"lang,attr,omitempty"`
+}
+
+type packetFormatter interface {
+ XMPPFormat() string
+}
diff --git a/xmpp/parser.go b/xmpp/parser.go
new file mode 100644
index 0000000..139cf22
--- /dev/null
+++ b/xmpp/parser.go
@@ -0,0 +1,96 @@
+package xmpp
+
+import (
+ "encoding/xml"
+ "errors"
+ "io"
+ "log"
+)
+
+// Reads and checks the opening XMPP stream element.
+// It returns a stream structure containing:
+// - Host: You can check the host against the host you were expecting to connect to
+// - Id: the Stream ID is a temporary shared secret used for some hash calculation. It is also used by ProcessOne
+// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
+// getting through the authentication process.
+func initDecoder(p *xml.Decoder) (sessionID string, err error) {
+ for {
+ var t xml.Token
+ t, err = p.Token()
+ if err != nil {
+ return
+ }
+
+ switch elem := t.(type) {
+ case xml.StartElement:
+ if elem.Name.Space != nsStream || elem.Name.Local != "stream" {
+ err = errors.New("xmpp: expected but got <" + elem.Name.Local + "> in " + elem.Name.Space)
+ return
+ }
+
+ // Parse Stream attributes
+ for _, attrs := range elem.Attr {
+ switch attrs.Name.Local {
+ case "id":
+ sessionID = attrs.Value
+ }
+ }
+ return
+ }
+ }
+ panic("unreachable")
+}
+
+// Scan XML token stream to find next StartElement.
+func nextStart(p *xml.Decoder) (xml.StartElement, error) {
+ for {
+ t, err := p.Token()
+ if err == io.EOF {
+ return xml.StartElement{}, nil
+ }
+ if err != nil {
+ log.Fatal("token:", err)
+ }
+ switch t := t.(type) {
+ case xml.StartElement:
+ return t, nil
+ }
+ }
+ panic("unreachable")
+}
+
+// Scan XML token stream for next element and save into val.
+// If val == nil, allocate new element based on proto map.
+// Either way, return val.
+func next(p *xml.Decoder) (xml.Name, interface{}, error) {
+ // Read start element to find out what type we want.
+ se, err := nextStart(p)
+ if err != nil {
+ return xml.Name{}, nil, err
+ }
+
+ // Put it in an interface and allocate one.
+ var nv interface{}
+ switch se.Name.Space + " " + se.Name.Local {
+ // TODO: general case = Parse IQ / presence / message => split SASL case
+ case nsSASL + " success":
+ nv = &saslSuccess{}
+ case nsSASL + " failure":
+ nv = &saslFailure{}
+ case nsClient + " message":
+ nv = &ClientMessage{}
+ case nsClient + " presence":
+ nv = &clientPresence{}
+ case nsClient + " iq":
+ nv = &clientIQ{}
+ default:
+ return xml.Name{}, nil, errors.New("unexpected XMPP message " +
+ se.Name.Space + " <" + se.Name.Local + "/>")
+ }
+
+ // Decode element into pointer storage
+ if err = p.DecodeElement(nv, &se); err != nil {
+ return xml.Name{}, nil, err
+ }
+ return se.Name, nv, err
+}
diff --git a/xmpp/presence.go b/xmpp/presence.go
new file mode 100644
index 0000000..3b071ab
--- /dev/null
+++ b/xmpp/presence.go
@@ -0,0 +1,13 @@
+package xmpp
+
+import "encoding/xml"
+
+// XMPP Packet Parsing
+type clientPresence struct {
+ XMLName xml.Name `xml:"jabber:client presence"`
+ Packet
+ Show string `xml:"show,attr,omitempty"` // away, chat, dnd, xa
+ Status string `xml:"status,attr,omitempty"`
+ Priority string `xml:"priority,attr,omitempty"`
+ //Error *clientError
+}
diff --git a/xmpp/session.go b/xmpp/session.go
new file mode 100644
index 0000000..a133e6c
--- /dev/null
+++ b/xmpp/session.go
@@ -0,0 +1,178 @@
+package xmpp
+
+import (
+ "crypto/tls"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+)
+
+const xmppStreamOpen = ""
+
+type Session struct {
+ // Session info
+ BindJid string // Jabber ID as provided by XMPP server
+ StreamId string
+ Features streamFeatures
+ TlsEnabled bool
+ lastPacketId int
+
+ // Session interface
+ In chan interface{}
+ Out chan interface{}
+
+ // read / write
+ socketProxy io.ReadWriter
+ decoder *xml.Decoder
+
+ // error management
+ err error
+}
+
+func NewSession(conn net.Conn, o Options) (net.Conn, *Session, error) {
+ s := new(Session)
+ s.init(conn, o)
+
+ // starttls
+ var tlsConn net.Conn
+ tlsConn = s.startTlsIfSupported(conn, o.parsedJid.domain)
+ s.reset(conn, tlsConn, o)
+
+ // auth
+ s.auth(o)
+ s.reset(tlsConn, tlsConn, o)
+
+ // bind resource and 'start' XMPP session
+ s.bind(o)
+ s.rfc3921Session(o)
+
+ return tlsConn, s, s.err
+}
+
+func (s *Session) PacketId() string {
+ s.lastPacketId++
+ return fmt.Sprintf("%x", s.lastPacketId)
+}
+
+func (s *Session) init(conn net.Conn, o Options) {
+ s.setProxy(nil, conn, o)
+ s.Features = s.open(o.parsedJid.domain)
+}
+
+func (s *Session) reset(conn net.Conn, newConn net.Conn, o Options) {
+ if s.err != nil {
+ return
+ }
+
+ s.setProxy(conn, newConn, o)
+ s.Features = s.open(o.parsedJid.domain)
+}
+
+// TODO: setProxyLogger ? better name ? This is not a TCP / HTTP proxy
+func (s *Session) setProxy(conn net.Conn, newConn net.Conn, o Options) {
+ if newConn != conn {
+ s.socketProxy = newSocketProxy(newConn, o.PacketLogger)
+ }
+ s.decoder = xml.NewDecoder(s.socketProxy)
+}
+
+func (s *Session) open(domain string) (f streamFeatures) {
+ // Send stream open tag
+ if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, nsClient, nsStream); s.err != nil {
+ return
+ }
+
+ // Set xml decoder and extract streamID from reply
+ s.StreamId, s.err = initDecoder(s.decoder) // TODO refactor / rename
+ if s.err != nil {
+ return
+ }
+
+ // extract stream features
+ if s.err = s.decoder.Decode(&f); s.err != nil {
+ s.err = errors.New("stream open decode features: " + s.err.Error())
+ }
+ return
+}
+
+func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
+ if s.err != nil {
+ return conn
+ }
+
+ if s.Features.StartTLS.XMLName.Space+" "+s.Features.StartTLS.XMLName.Local == nsTLS+" starttls" {
+ fmt.Fprintf(s.socketProxy, "")
+
+ var k tlsProceed
+ if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
+ s.err = errors.New("expecting starttls proceed: " + s.err.Error())
+ return conn
+ }
+ s.TlsEnabled = true
+
+ // TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
+ DefaultTlsConfig.ServerName = domain
+ var tlsConn *tls.Conn = tls.Client(conn, &DefaultTlsConfig)
+ // We convert existing connection to TLS
+ if s.err = tlsConn.Handshake(); s.err != nil {
+ return tlsConn
+ }
+
+ // We check that cert matches hostname
+ s.err = tlsConn.VerifyHostname(domain)
+ return tlsConn
+ }
+
+ // starttls is not supported => we do not upgrade the connection:
+ return conn
+}
+
+func (s *Session) auth(o Options) {
+ if s.err != nil {
+ return
+ }
+
+ s.err = authSASL(s.socketProxy, s.decoder, s.Features, o.parsedJid.username, o.Password)
+}
+
+func (s *Session) bind(o Options) {
+ if s.err != nil {
+ return
+ }
+
+ // Send IQ message asking to bind to the local user name.
+ var resource = o.parsedJid.resource
+ if resource != "" {
+ fmt.Fprintf(s.socketProxy, "%s",
+ s.PacketId(), nsBind, resource)
+ } else {
+ fmt.Fprintf(s.socketProxy, "", s.PacketId(), nsBind)
+ }
+
+ var iq clientIQ
+ if s.err = s.decoder.Decode(&iq); s.err != nil || &iq.Bind == nil {
+ s.err = errors.New("iq bind result missing: " + s.err.Error())
+ return
+ }
+ s.BindJid = iq.Bind.Jid // our local id (with possibly randomly generated resource
+ return
+}
+
+// TODO: remove when ejabberd is fixed: https://github.com/processone/ejabberd/issues/869
+// After the bind, if the session is required (as per old RFC 3921), we send the session open iq
+func (s *Session) rfc3921Session(o Options) {
+ if s.err != nil {
+ return
+ }
+
+ var iq clientIQ
+
+ // TODO: Do no send unconditionally, check if session is optional and omit it
+ fmt.Fprintf(s.socketProxy, "", s.PacketId(), nsSession)
+ if s.err = s.decoder.Decode(&iq); s.err != nil {
+ s.err = errors.New("expecting iq result after session open: " + s.err.Error())
+ return
+ }
+}
diff --git a/xmpp/socket_proxy.go b/xmpp/socket_proxy.go
new file mode 100644
index 0000000..59a3ee3
--- /dev/null
+++ b/xmpp/socket_proxy.go
@@ -0,0 +1,49 @@
+package xmpp
+
+import (
+ "io"
+ "os"
+)
+
+// Mediated Read / Write on socket
+// Used if logFile from Options is not nil
+type socketProxy struct {
+ socket io.ReadWriter // Actual connection
+ logFile *os.File
+}
+
+func newSocketProxy(conn io.ReadWriter, logFile *os.File) io.ReadWriter {
+ if logFile == nil {
+ return conn
+ } else {
+ return &socketProxy{conn, logFile}
+ }
+}
+
+func (pl *socketProxy) Read(p []byte) (n int, err error) {
+ n, err = pl.socket.Read(p)
+ if n > 0 {
+ pl.logFile.Write([]byte("RECV:\n")) // Prefix
+ if n, err := pl.logFile.Write(p[:n]); err != nil {
+ return n, err
+ }
+ pl.logFile.Write([]byte("\n\n")) // Separator
+ }
+ return
+}
+
+func (pl *socketProxy) Write(p []byte) (n int, err error) {
+ pl.logFile.Write([]byte("SEND:\n")) // Prefix
+ for _, w := range []io.Writer{pl.socket, pl.logFile} {
+ n, err = w.Write(p)
+ if err != nil {
+ return
+ }
+ if n != len(p) {
+ err = io.ErrShortWrite
+ return
+ }
+ }
+ pl.logFile.Write([]byte("\n\n")) // Separator
+ return len(p), nil
+}
diff --git a/xmpp/starttls.go b/xmpp/starttls.go
new file mode 100644
index 0000000..fef56ee
--- /dev/null
+++ b/xmpp/starttls.go
@@ -0,0 +1,22 @@
+package xmpp
+
+import (
+ "crypto/tls"
+ "encoding/xml"
+)
+
+var DefaultTlsConfig tls.Config
+
+// XMPP Packet Parsing
+type tlsStartTLS struct {
+ XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
+ Required bool
+}
+
+type tlsProceed struct {
+ XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
+}
+
+type tlsFailure struct {
+ XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
+}
diff --git a/xmpp/stream.go b/xmpp/stream.go
new file mode 100644
index 0000000..b83ccc8
--- /dev/null
+++ b/xmpp/stream.go
@@ -0,0 +1,22 @@
+package xmpp
+
+import "encoding/xml"
+
+// XMPP Packet Parsing
+type streamFeatures struct {
+ XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
+ StartTLS tlsStartTLS
+ Caps Caps
+ Mechanisms saslMechanisms
+ Bind bindBind
+ Session sessionSession
+ Any []xml.Name `xml:",any"`
+}
+
+type Caps struct {
+ XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
+ Hash string `xml:"hash,attr"`
+ Node string `xml:"node,attr"`
+ Ver string `xml:"ver,attr"`
+ Ext string `xml:"ext,attr,omitempty"`
+}
diff --git a/xmpp_client.go b/xmpp_client.go
new file mode 100644
index 0000000..5a10de2
--- /dev/null
+++ b/xmpp_client.go
@@ -0,0 +1,47 @@
+/*
+xmpp_client is a demo client that connect on an XMPP server and echo message received back to original sender.
+*/
+
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/mremond/gox/xmpp"
+)
+
+func main() {
+ options := xmpp.Options{Address: "localhost:5222", Jid: "test@localhost", Password: "test", PacketLogger: os.Stdout}
+
+ var client *xmpp.Client
+ var err error
+ if client, err = xmpp.NewClient(options); err != nil {
+ log.Fatal("Error: ", err)
+ }
+
+ var session *xmpp.Session
+ if session, err = client.Connect(); err != nil {
+ log.Fatal("Error: ", err)
+ }
+
+ fmt.Println("Stream opened, we have streamID = ", session.StreamId)
+
+ // Iterator to receive packets coming from our XMPP connection
+ for packet := range client.Recv() {
+ switch packet := packet.(type) {
+ case *xmpp.ClientMessage:
+ fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From)
+ reply := xmpp.ClientMessage{Packet: xmpp.Packet{To: packet.From}, Body: packet.Body}
+ client.Send(reply.XMPPFormat())
+ default:
+ fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
+ }
+ }
+}
+
+// TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
+// (using templates ?)
+
+// TODO: autoreconnect when connection is lost