Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add String() method to Response for logging purposes #12

Merged
merged 1 commit into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 71 additions & 4 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,89 @@ 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:
return true
}
}

// 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}
Expand Down
48 changes: 48 additions & 0 deletions response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}