1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819// Package parser provides utilities for parsing of structured log messsages20// generated by maddy.21package parser2223import (24 "encoding/json"25 "strings"26 "time"27 "unicode"28)2930type (31 Msg struct {32 Stamp time.Time33 Debug bool34 Module string35 Message string36 Context map[string]interface{}37 }3839 MalformedMsg struct {40 Desc string41 Err error42 }43)4445const (46 ISO8601_UTC = "2006-01-02T15:04:05.000Z"47)4849func (m MalformedMsg) Error() string {50 if m.Err != nil {51 return "parse: " + m.Desc + ": " + m.Err.Error()52 }53 return "parse: " + m.Desc54}5556// Parse parses the message from the maddy log file.57//58// It assumes standard file output, including the [debug] tag and59// ISO 8601 timestamp at the start of each line. Timestamp is assumed to be in60// 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 }7172 m := Msg{73 Context: map[string]interface{}{},74 }7576 // After that, the second part is the context. It can be empty, so don't fail77 // 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 }8384 // 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 }9091 var err error92 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 }9697 msgText := msgParts[1]98 if strings.HasPrefix(msgText, "[debug] ") {99 msgText = strings.TrimPrefix(msgText, "[debug] ")100 m.Debug = true101 }102103 moduleText := strings.SplitN(msgText, ": ", 2)104 if len(moduleText) == 1 {105 // No module prefix, that's fine.106 m.Message = msgText107 return m, nil108 }109110 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 = msgText116 return m, nil117 }118 }119120 m.Module = moduleText[0]121 m.Message = moduleText[1]122123 return m, nil124}