diff --git a/response.go b/response.go index 5c984d4..3c1f410 100644 --- a/response.go +++ b/response.go @@ -17,15 +17,15 @@ type Response struct { } // Response returns message instance with data -func (c *Response) Response() *wire.Message { - return &wire.Message{Code: c.code, Data: c.data} +func (r *Response) Response() *wire.Message { + return &wire.Message{Code: r.code, Data: r.data} } // Continue returns false if the MTA should stop sending events for this transaction, true otherwise. // A [RespDiscard] Response will return false because the MTA should end sending events for the current // SMTP transaction to this milter. -func (c *Response) Continue() bool { - switch wire.ActionCode(c.code) { +func (r *Response) Continue() bool { + switch wire.ActionCode(r.code) { case wire.ActAccept, wire.ActDiscard, wire.ActReject, wire.ActTempFail, wire.ActReplyCode: return false default: @@ -33,6 +33,73 @@ func (c *Response) Continue() bool { } } +// String returns a string representation of this response. +// Can be used for logging purposes. +// This method will always return a logfmt compatible string. +// We try to not alter the output of this method arbitrarily – but we do not make any guaranties. +// +// It sometimes internally examines the bytes that will be sent over the wire with the parsing code +// of the client part of this library. This is not the most performant implementation, so +// you might opt to not use this method when your code needs to be performant. +func (r *Response) String() string { + switch wire.ActionCode(r.code) { + case wire.ActContinue: + return "response=continue" + case wire.ActAccept: + return "response=accept" + case wire.ActDiscard: + return "response=discard" + case wire.ActReject: + return "response=reject" + case wire.ActTempFail: + return "response=temp_fail" + case wire.ActSkip: + return "response=skip" + case wire.ActProgress: + return "response=progress" + case wire.ActReplyCode: + act, err := parseAction(r.Response()) + if err != nil { + return fmt.Sprintf("response=invalid code=%d data_len=%d data=%q", r.code, len(r.data), r.data) + } + action := "temp_fail" + if act.SMTPCode > 499 { + action = "reject" + } + return fmt.Sprintf("response=reply_code action=%s code=%d reason=%q", action, act.SMTPCode, act.SMTPReply) + } + // Users of the library do not really see modification Response objects. + // This is just for completeness’ sake + act, err := parseModifyAct(r.Response()) + if err == nil { + switch act.Type { + case ActionAddRcpt: + if act.RcptArgs != "" { + return fmt.Sprintf("response=add_rcpt rcpt=%q args=%q", act.Rcpt, act.RcptArgs) + } + return fmt.Sprintf("response=add_rcpt rcpt=%q", act.Rcpt) + case ActionDelRcpt: + return fmt.Sprintf("response=del_rcpt rcpt=%q", act.Rcpt) + case ActionQuarantine: + return fmt.Sprintf("response=quarantine reason=%q", act.Reason) + case ActionReplaceBody: + return fmt.Sprintf("response=replace_body len=%d", len(act.Body)) + case ActionChangeFrom: + if act.FromArgs != "" { + return fmt.Sprintf("response=change_from from=%q args=%q", act.From, act.FromArgs) + } + return fmt.Sprintf("response=change_from from=%q", act.From) + case ActionAddHeader: + return fmt.Sprintf("response=add_header name=%q value=%q", act.HeaderName, act.HeaderValue) + case ActionChangeHeader: + return fmt.Sprintf("response=change_header name=%q value=%q index=%d", act.HeaderName, act.HeaderValue, act.HeaderIndex) + case ActionInsertHeader: + return fmt.Sprintf("response=insert_header name=%q value=%q index=%d", act.HeaderName, act.HeaderValue, act.HeaderIndex) + } + } + return fmt.Sprintf("response=unknown code=%d data_len=%d data=%q", r.code, len(r.data), r.data) +} + // newResponse generates a new Response suitable for [wire.WritePacket] func newResponse(code wire.Code, data []byte) *Response { return &Response{code, data} diff --git a/response_test.go b/response_test.go index d1dbdd6..b978a97 100644 --- a/response_test.go +++ b/response_test.go @@ -86,3 +86,51 @@ func TestCustomResponseDefaultResponse(t *testing.T) { }) } } + +func TestResponse_String(t *testing.T) { + type fields struct { + code wire.Code + data []byte + } + tests := []struct { + name string + fields fields + want string + }{ + {"continue", fields{wire.Code(wire.ActContinue), nil}, "response=continue"}, + {"accept", fields{wire.Code(wire.ActAccept), nil}, "response=accept"}, + {"discard", fields{wire.Code(wire.ActDiscard), nil}, "response=discard"}, + {"reject", fields{wire.Code(wire.ActReject), nil}, "response=reject"}, + {"temp_fail", fields{wire.Code(wire.ActTempFail), nil}, "response=temp_fail"}, + {"skip", fields{wire.Code(wire.ActSkip), nil}, "response=skip"}, + {"progress", fields{wire.Code(wire.ActProgress), nil}, "response=progress"}, + {"reply_code1", fields{wire.Code(wire.ActReplyCode), []byte("444 test\x00")}, "response=reply_code action=temp_fail code=444 reason=\"444 test\""}, + {"reply_code2", fields{wire.Code(wire.ActReplyCode), []byte("555 test\x00")}, "response=reply_code action=reject code=555 reason=\"555 test\""}, + {"reply_code3", fields{wire.Code(wire.ActReplyCode), []byte("continue\x00")}, "response=invalid code=121 data_len=9 data=\"continue\\x00\""}, + {"add_rcpt1", fields{wire.Code(wire.ActAddRcpt), []byte("<>\x00")}, "response=add_rcpt rcpt=\"<>\""}, + {"add_rcpt2", fields{wire.Code(wire.ActAddRcptPar), []byte("<>\x00A=B\x00")}, "response=add_rcpt rcpt=\"<>\" args=\"A=B\""}, + {"del_rcpt", fields{wire.Code(wire.ActDelRcpt), []byte("<>\x00A=B\x00")}, "response=del_rcpt rcpt=\"<>\""}, + {"quarantine", fields{wire.Code(wire.ActQuarantine), []byte("spam\x00")}, "response=quarantine reason=\"spam\""}, + {"replace_body", fields{wire.Code(wire.ActReplBody), []byte("1234")}, "response=replace_body len=4"}, + {"change_from1", fields{wire.Code(wire.ActChangeFrom), []byte("<>\x00")}, "response=change_from from=\"<>\""}, + {"change_from2", fields{wire.Code(wire.ActChangeFrom), []byte("<>\x00A=B\x00")}, "response=change_from from=\"<>\" args=\"A=B\""}, + {"add_header", fields{wire.Code(wire.ActAddHeader), []byte("X-Test\x00Test\x00")}, "response=add_header name=\"X-Test\" value=\"Test\""}, + {"change_header", fields{wire.Code(wire.ActChangeHeader), []byte("\x00\x00\x00\x01X-Test\x00Test\x00")}, "response=change_header name=\"X-Test\" value=\"Test\" index=1"}, + {"insert_header", fields{wire.Code(wire.ActInsertHeader), []byte("\x00\x00\x00\x01X-Test\x00Test\x00")}, "response=insert_header name=\"X-Test\" value=\"Test\" index=1"}, + {"garbage", fields{wire.Code(0), []byte("\x00\x00\x00\x00")}, "response=unknown code=0 data_len=4 data=\"\\x00\\x00\\x00\\x00\""}, + {"garbage-nil", fields{wire.Code(128), nil}, "response=unknown code=128 data_len=0 data=\"\""}, + } + for _, tt_ := range tests { + t.Run(tt_.name, func(t *testing.T) { + tt := tt_ + t.Parallel() + r := &Response{ + code: tt.fields.code, + data: tt.fields.data, + } + if got := r.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +}