340 lines
8.1 KiB
Go
340 lines
8.1 KiB
Go
// Message bundles manipulations (encode/decode).
|
|
// Decode message from user (point).
|
|
// Some validation functions.
|
|
|
|
package ii
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// II-tags, encoded in raw message as key1/value1/key2/value2.. string
|
|
// When message is decoded into Msg,
|
|
// key/value properties of tags associated with it.
|
|
// When encoding Msg, all properties will translated to tags string
|
|
// List - is the names of properties
|
|
// Hash - is the map of properties (Name->Value)
|
|
type Tags struct {
|
|
Hash map[string]string
|
|
List []string
|
|
}
|
|
|
|
// Decoded message.
|
|
// Has all atrributes of message
|
|
// including Tags.
|
|
type Msg struct {
|
|
MsgId string
|
|
Tags Tags
|
|
Echo string
|
|
Date int64
|
|
From string
|
|
Addr string
|
|
To string
|
|
Subj string
|
|
Text string
|
|
}
|
|
|
|
// Make MsgId from raw text
|
|
// MsgId is unique identificator of message
|
|
// It is supposed that there is no collision of MsgId
|
|
// It is base64(sha256(text)) transformation
|
|
func MsgId(msg string) string {
|
|
h := sha256.Sum256([]byte(msg))
|
|
id := base64.StdEncoding.EncodeToString(h[:])
|
|
id = strings.Replace(id, "+", "A", -1)
|
|
id = strings.Replace(id, "/", "Z", -1)
|
|
return id[0:20]
|
|
}
|
|
|
|
// Check if string is valid MsgId
|
|
func IsMsgId(id string) bool {
|
|
return len(id) == 20 && !strings.Contains(id, ".")
|
|
}
|
|
|
|
// Check if Echoarea is private area
|
|
// This is openidec extension, echoareas
|
|
// that has "." prefix are for private messaging.
|
|
// Those areas can be fetched only with /u/point/auth/u/e/ scheme
|
|
func IsPrivate(e string) bool {
|
|
return strings.HasPrefix(e, ".")
|
|
}
|
|
|
|
// Check if string is valid echoarea name
|
|
func IsEcho(e string) bool {
|
|
l := len(e)
|
|
return l >= 3 && l <= 120 && strings.Contains(e, ".") && !strings.Contains(e, ":")
|
|
}
|
|
|
|
// Check if string is valid subject
|
|
// In fact, it is just return true stub :)
|
|
func IsSubject(s string) bool {
|
|
return true // len(strings.TrimSpace(s)) > 0
|
|
}
|
|
|
|
// Check if subject is empty string
|
|
// Used when validate msg from points
|
|
func IsEmptySubject(s string) bool {
|
|
return len(strings.TrimSpace(s)) > 0
|
|
}
|
|
|
|
// Decode message from point sent with /u/point scheme.
|
|
// Try to use URL save and STD base64.
|
|
// Returns pointrt to decoded Msg or nil (and error)
|
|
// Note: This function adds "ii/ok" to Tags and
|
|
// set Date field with UTC Unix time.
|
|
func DecodeMsgline(msg string, enc bool) (*Msg, error) {
|
|
var m Msg
|
|
var data []byte
|
|
var err error
|
|
if len(msg) > 65536 {
|
|
return nil, errors.New("Message too long")
|
|
}
|
|
if enc {
|
|
if data, err = base64.StdEncoding.DecodeString(msg); err != nil {
|
|
if data, err = base64.URLEncoding.DecodeString(msg); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else {
|
|
data = []byte(msg)
|
|
}
|
|
text := strings.Split(string(data), "\n")
|
|
if len(text) < 5 {
|
|
return nil, errors.New("Wrong message format")
|
|
}
|
|
if text[3] != "" {
|
|
return nil, errors.New("No body delimiter in message")
|
|
}
|
|
m.Echo = strings.TrimSpace(text[0])
|
|
if !IsEcho(m.Echo) {
|
|
return nil, errors.New("Wrong echoarea format")
|
|
}
|
|
m.To = strings.TrimSpace(text[1])
|
|
if len(m.To) == 0 {
|
|
m.To = "All"
|
|
}
|
|
m.Subj = strings.TrimSpace(text[2])
|
|
if !IsEmptySubject(m.Subj) {
|
|
return nil, errors.New("Wrong subject")
|
|
}
|
|
m.Date = time.Now().Unix()
|
|
start := 4
|
|
repto := text[4]
|
|
m.Tags, _ = MakeTags("ii/ok")
|
|
if strings.HasPrefix(repto, "@repto:") {
|
|
start += 1
|
|
repto = strings.Trim(strings.Split(repto, ":")[1], " ")
|
|
if err := m.Tags.Add("repto/" + repto); err != nil {
|
|
return nil, err
|
|
}
|
|
Trace.Printf("Add repto tag: %s", repto)
|
|
}
|
|
for i := start; i < len(text); i++ {
|
|
m.Text += text[i] + "\n"
|
|
}
|
|
m.Text = strings.TrimSuffix(m.Text, "\n")
|
|
Trace.Printf("Final message: %s\n", m.String())
|
|
return &m, nil
|
|
}
|
|
|
|
// Decode bundle line in msgid:message format or just message
|
|
// Returns pointer to decoded Msg or nil, error if fail.
|
|
// Can parse URL safe and STD BASE64.
|
|
// This function does NOT add ii/ok tag and does NOT change Date
|
|
func DecodeBundle(msg string) (*Msg, error) {
|
|
var m Msg
|
|
if strings.Contains(msg, ":") {
|
|
spl := strings.Split(msg, ":")
|
|
if len(spl) != 2 {
|
|
return nil, errors.New("Wrong bundle format")
|
|
}
|
|
msg = spl[1]
|
|
m.MsgId = spl[0]
|
|
if !IsMsgId(m.MsgId) {
|
|
return nil, errors.New("Wrong MsgId format")
|
|
}
|
|
}
|
|
msg = strings.Replace(msg, "-", "+", -1) /* if it is URL base64 */
|
|
msg = strings.Replace(msg, "_", "/", -1) /* make it base64 */
|
|
data, err := base64.StdEncoding.DecodeString(msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if m.MsgId == "" {
|
|
m.MsgId = MsgId(string(data))
|
|
}
|
|
text := strings.Split(string(data), "\n")
|
|
if len(text) <= 8 {
|
|
return nil, errors.New("Wrong message format")
|
|
}
|
|
m.Tags, err = MakeTags(text[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.Echo = text[1]
|
|
if !IsEcho(m.Echo) {
|
|
return nil, errors.New("Wrong echoarea format")
|
|
}
|
|
_, err = fmt.Sscanf(text[2], "%d", &m.Date)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.From = text[3]
|
|
m.Addr = text[4]
|
|
m.To = text[5]
|
|
m.Subj = text[6]
|
|
if !IsSubject(m.Subj) {
|
|
return nil, errors.New("Wrong subject")
|
|
}
|
|
for i := 8; i < len(text); i++ {
|
|
m.Text += text[i] + "\n"
|
|
}
|
|
m.Text = strings.TrimSuffix(m.Text, "\n")
|
|
return &m, nil
|
|
}
|
|
|
|
// Creates Tags from string in key1/value1/key2/value2/... format
|
|
// Can return error (with unfilled Tags) if format is wrong.
|
|
func MakeTags(str string) (Tags, error) {
|
|
var t Tags
|
|
str = strings.Trim(str, " ")
|
|
if str == "" { // empty
|
|
return t, nil
|
|
}
|
|
tags := strings.Split(str, "/")
|
|
if len(tags)%2 != 0 {
|
|
return t, errors.New("Wrong tags: " + str)
|
|
}
|
|
t.Hash = make(map[string]string)
|
|
for i := 0; i < len(tags); i += 2 {
|
|
t.Hash[tags[i]] = tags[i+1]
|
|
t.List = append(t.List, tags[i])
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// Create Tags from string in key1/value1/key2/value2/... format
|
|
// ignoring errors. This is useful for creating new "ii/ok" tag.
|
|
func NewTags(str string) Tags {
|
|
t, _ := MakeTags(str)
|
|
return t
|
|
}
|
|
|
|
// Returns Tags propertie with name n.
|
|
// Returns "", false if such propertie does not exists in Tags.
|
|
func (t *Tags) Get(n string) (string, bool) {
|
|
if t == nil || t.Hash == nil {
|
|
return "", false
|
|
}
|
|
v, ok := t.Hash[n]
|
|
if ok {
|
|
return v, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// Add tags in key/value/... format to existing Tags.
|
|
func (t *Tags) Add(str string) error {
|
|
tags := strings.Split(str, "/")
|
|
if len(tags)%2 != 0 {
|
|
return errors.New("Wrong tags")
|
|
}
|
|
if t.Hash == nil {
|
|
t.Hash = make(map[string]string)
|
|
}
|
|
for i := 0; i < len(tags); i += 2 {
|
|
_, ok := t.Hash[tags[i]]
|
|
t.Hash[tags[i]] = tags[i+1]
|
|
if !ok { /* new tag */
|
|
t.List = append(t.List, tags[i])
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Remove tag with name tag from Tags.
|
|
func (t *Tags) Del(tag string) bool {
|
|
if t.Hash == nil {
|
|
return false
|
|
}
|
|
_, ok := t.Hash[tag]
|
|
if !ok {
|
|
return false
|
|
}
|
|
delete(t.Hash, tag)
|
|
for k, v := range t.List {
|
|
if v == tag {
|
|
copy(t.List[k:], t.List[k+1:])
|
|
t.List[len(t.List)-1] = ""
|
|
t.List = t.List[:len(t.List)-1]
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Translate Tags to string in key1/value1/key2/value2/... format.
|
|
func (t Tags) String() string {
|
|
var text string
|
|
if t.Hash == nil {
|
|
return ""
|
|
}
|
|
for _, n := range t.List {
|
|
if val, ok := t.Hash[n]; ok {
|
|
text += fmt.Sprintf("%s/%s/", n, val)
|
|
}
|
|
}
|
|
text = strings.TrimSuffix(text, "/")
|
|
return text
|
|
}
|
|
|
|
// Dump (returns string) decoded message for debug purposes.
|
|
func (m *Msg) Dump() string {
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("id: %s\ntags: %s\nechoarea: %s\ndate: %s\nmsgfrom: %s\naddr: %s\nmsgto: %s\nsubj: %s\n\n%s",
|
|
m.MsgId, m.Tags.String(), m.Echo, time.Unix(m.Date, 0), m.From, m.Addr, m.To, m.Subj, m.Text)
|
|
}
|
|
|
|
// Get if tag property with name n is associated with Msg
|
|
func (m *Msg) Tag(n string) (string, bool) {
|
|
return m.Tags.Get(n)
|
|
}
|
|
|
|
// Translate decoded Msg to raw text format ready to encoding.
|
|
func (m *Msg) String() string {
|
|
tags := m.Tags.String()
|
|
text := strings.Join([]string{tags, m.Echo,
|
|
fmt.Sprint(m.Date),
|
|
m.From,
|
|
m.Addr,
|
|
m.To,
|
|
m.Subj,
|
|
"",
|
|
m.Text}, "\n")
|
|
return text
|
|
}
|
|
|
|
// Encode Msg into bundle format (msgid:base64text).
|
|
func (m *Msg) Encode() string {
|
|
var text string
|
|
if m == nil || m.Echo == "" {
|
|
return ""
|
|
}
|
|
if m.Date == 0 {
|
|
now := time.Now()
|
|
m.Date = now.Unix()
|
|
}
|
|
text = m.String()
|
|
if m.MsgId == "" {
|
|
m.MsgId = MsgId(text)
|
|
}
|
|
return m.MsgId + ":" + base64.StdEncoding.EncodeToString([]byte(text))
|
|
}
|