maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19// Package parser provides utilities for parsing of structured log messsages
 20// generated by maddy.
 21package parser
 22
 23import (
 24	"encoding/json"
 25	"strings"
 26	"time"
 27	"unicode"
 28)
 29
 30type (
 31	Msg struct {
 32		Stamp   time.Time
 33		Debug   bool
 34		Module  string
 35		Message string
 36		Context map[string]interface{}
 37	}
 38
 39	MalformedMsg struct {
 40		Desc string
 41		Err  error
 42	}
 43)
 44
 45const (
 46	ISO8601_UTC = "2006-01-02T15:04:05.000Z"
 47)
 48
 49func (m MalformedMsg) Error() string {
 50	if m.Err != nil {
 51		return "parse: " + m.Desc + ": " + m.Err.Error()
 52	}
 53	return "parse: " + m.Desc
 54}
 55
 56// Parse parses the message from the maddy log file.
 57//
 58// It assumes standard file output, including the [debug] tag and
 59// ISO 8601 timestamp at the start of each line. Timestamp is assumed to be in
 60// the UTC, as it is enforced by maddy.
 61//
 62// JSON context values are unmarshalled without any additional processing,
 63// notably that means that all numbers are represented as float64.
 64func Parse(line string) (Msg, error) {
 65	parts := strings.Split(line, "\t")
 66	if len(parts) != 2 {
 67		// All messages even without a Context have a trailing \t,
 68		// so this one is obviously malformed.
 69		return Msg{}, MalformedMsg{Desc: "missing a tab separator"}
 70	}
 71
 72	m := Msg{
 73		Context: map[string]interface{}{},
 74	}
 75
 76	// After that, the second part is the context. It can be empty, so don't fail
 77	// if there is none.
 78	if len(parts[1]) != 0 {
 79		if err := json.Unmarshal([]byte(parts[1]), &m.Context); err != nil {
 80			return Msg{}, MalformedMsg{Desc: "context unmarshal", Err: err}
 81		}
 82	}
 83
 84	// Okay, the first one might contain the timestamp at start.
 85	// Cut it away.
 86	msgParts := strings.SplitN(parts[0], " ", 2)
 87	if len(msgParts) == 1 {
 88		return Msg{}, MalformedMsg{Desc: "missing a timestamp"}
 89	}
 90
 91	var err error
 92	m.Stamp, err = time.ParseInLocation(ISO8601_UTC, msgParts[0], time.UTC)
 93	if err != nil {
 94		return Msg{}, MalformedMsg{Desc: "timestamp parse", Err: err}
 95	}
 96
 97	msgText := msgParts[1]
 98	if strings.HasPrefix(msgText, "[debug] ") {
 99		msgText = strings.TrimPrefix(msgText, "[debug] ")
100		m.Debug = true
101	}
102
103	moduleText := strings.SplitN(msgText, ": ", 2)
104	if len(moduleText) == 1 {
105		// No module prefix, that's fine.
106		m.Message = msgText
107		return m, nil
108	}
109
110	for _, ch := range moduleText[0] {
111		switch {
112		case unicode.IsDigit(ch), unicode.IsLetter(ch), ch == '/':
113		default:
114			// This is not a module prefix, don't treat it as such.
115			m.Message = msgText
116			return m, nil
117		}
118	}
119
120	m.Module = moduleText[0]
121	m.Message = moduleText[1]
122
123	return m, nil
124}