Skip to content

Commit 278329c

Browse files
authored
Merge pull request #179 from mocktools/develop
Golang smtpmock v2.3.0
2 parents d91c065 + 376a658 commit 278329c

File tree

13 files changed

+228
-60
lines changed

13 files changed

+228
-60
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
3+
enableGlobDot: true
4+
5+
patterns:
6+
- name: GithubUser
7+
pattern: /\[@.+\]/gmx
8+
9+
languageSettings:
10+
- languageId: markdown
11+
ignoreRegExpList:
12+
- Email
13+
- GithubUser
14+
15+
words:
16+
- autobuilds
17+
- bestwebua
18+
- codecov
19+
- codesmells
20+
- commitspell
21+
- consts
22+
- crtypto
23+
- funcs
24+
- unexported
25+
- golangci
26+
- gomod
27+
- gotestsum
28+
- goreleaser
29+
- lefthook
30+
- ldflags
31+
- markdownlint
32+
- mocktools
33+
- punycode
34+
- rubocop
35+
- shellcheck
36+
- shortcuting
37+
- smtpmock
38+
- sigquit
39+
- struct
40+
- structs
41+
- yamlint
42+
- rset
43+
- rcptto
44+
- helo

.circleci/linter_configs/.lefthook.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ skip_output:
66

77
linters:
88
commands:
9+
commitspell:
10+
run: .circleci/scripts/commitspell.sh -c '.circleci/linter_configs/.commitspell.yml'
911
cspell:
1012
run: cspell-cli lint -c '.circleci/linter_configs/.cspell.yml' '**/*.{txt,md}'
1113
golangci:

.circleci/scripts/commitspell.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/sh
2+
set -e
3+
4+
configuration=$(if [ "$2" = "" ]; then echo "$2"; else echo " $1 $2"; fi)
5+
latest_commit=$(git rev-parse HEAD)
6+
7+
spellcheck_info() {
8+
echo "Checking the spelling of the latest commit ($latest_commit) message..."
9+
}
10+
11+
compose_cspell_command() {
12+
echo "cspell-cli lint stdin$configuration"
13+
}
14+
15+
cspell="$(compose_cspell_command)"
16+
17+
spellcheck_latest_commit() {
18+
git log -1 --pretty=%B | $cspell
19+
}
20+
21+
spellcheck_info
22+
spellcheck_latest_commit

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
golang 1.21.5
1+
golang 1.22.0

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
44

5+
## [2.3.0] - 2024-03-03
6+
7+
### Added
8+
9+
- Added ability to message purge when retrieving messages, `server.MessagesAndPurge()`. Thanks [@mitar](https://github.com/mitar) for PR
10+
- Added `commitspell` linter
11+
12+
### Fixed
13+
14+
- Fixed issue with data race condition between newMessage() and Messages(). Thanks [@mitar](https://github.com/mitar) for PR
15+
16+
### Updated
17+
18+
- Updated `lefthook` config
19+
- Updated project documentation
20+
521
## [2.2.1] - 2024-01-25
622

723
### Added

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,13 @@ func main() {
314314
client.Close()
315315

316316
// Each result of SMTP session will be saved as message.
317-
// To get access to server messages use Messages() method
317+
// To get access for server messages copies use Messages() method
318318
server.Messages()
319319

320+
// To get access for server messages copies and purge it on server after
321+
// use MessagesAndPurge() method
322+
server.MessagesAndPurge()
323+
320324
// To stop the server use Stop() method. Please note, smtpmock uses graceful shutdown.
321325
// It means that smtpmock will end all sessions after client responses or by session
322326
// timeouts immediately.

handler_rset.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func (handler *handlerRset) run(request string) {
3131
func (handler *handlerRset) clearMessage() {
3232
messageWithData, configuration := handler.message, handler.configuration
3333

34-
if !(configuration.multipleMessageReceiving && messageWithData.isConsistent()) {
34+
if !(configuration.multipleMessageReceiving && messageWithData.IsConsistent()) {
3535
clearedMessage := &Message{
3636
heloRequest: messageWithData.heloRequest,
3737
heloResponse: messageWithData.heloResponse,

message.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,6 @@ func (message Message) IsConsistent() bool {
121121
return message.mailfrom && message.rcptto && message.data && message.msg
122122
}
123123

124-
// Message pointer consistency status predicate. Returns true for case
125-
// when message struct is consistent. It means that MAILFROM, RCPTTO, DATA
126-
// commands and message context were successful. Otherwise returns false
127-
func (message *Message) isConsistent() bool {
128-
return message.mailfrom && message.rcptto && message.data && message.msg
129-
}
130-
131124
// Message RCPTTO successful response predicate. Returns true when at least one
132125
// successful RCPTTO response exists. Otherwise returns false
133126
func (message *Message) isIncludesSuccessfulRcpttoResponse(targetSuccessfulResponse string) bool {
@@ -145,16 +138,43 @@ var zeroMessage = &Message{}
145138

146139
// Concurrent type that can be safely shared between goroutines
147140
type messages struct {
148-
sync.Mutex
141+
sync.RWMutex
149142
items []*Message
150143
}
151144

152145
// messages methods
153146

154-
// Addes new message pointer into concurrent messages slice
147+
// Adds new message pointer into concurrent messages slice
155148
func (messages *messages) append(item *Message) {
156149
messages.Lock()
157150
defer messages.Unlock()
158-
159151
messages.items = append(messages.items, item)
160152
}
153+
154+
// Returns a copy of all messages
155+
func (messages *messages) copy() []Message {
156+
messages.RLock()
157+
defer messages.RUnlock()
158+
return messages.copyInternal()
159+
}
160+
161+
// Copy messages without a lock
162+
func (messages *messages) copyInternal() []Message {
163+
copiedMessages := []Message{}
164+
for index := range messages.items {
165+
copiedMessages = append(copiedMessages, *messages.items[index])
166+
}
167+
168+
return copiedMessages
169+
}
170+
171+
// Returns all messages and removes them at the same time
172+
func (messages *messages) purge() []Message {
173+
messages.Lock()
174+
defer messages.Unlock()
175+
176+
copiedMessages := messages.copyInternal()
177+
messages.items = nil
178+
179+
return copiedMessages
180+
}

message_test.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,30 +193,30 @@ func TestMessagePointerIsConsistent(t *testing.T) {
193193
t.Run("when consistent", func(t *testing.T) {
194194
message := &Message{mailfrom: true, rcptto: true, data: true, msg: true}
195195

196-
assert.True(t, message.isConsistent())
196+
assert.True(t, message.IsConsistent())
197197
})
198198

199199
t.Run("when not consistent MAILFROM", func(t *testing.T) {
200200

201-
assert.False(t, new(Message).isConsistent())
201+
assert.False(t, new(Message).IsConsistent())
202202
})
203203

204204
t.Run("when not consistent RCPTTO", func(t *testing.T) {
205205
message := &Message{mailfrom: true}
206206

207-
assert.False(t, message.isConsistent())
207+
assert.False(t, message.IsConsistent())
208208
})
209209

210210
t.Run("when not consistent DATA", func(t *testing.T) {
211211
message := &Message{mailfrom: true, rcptto: true}
212212

213-
assert.False(t, message.isConsistent())
213+
assert.False(t, message.IsConsistent())
214214
})
215215

216216
t.Run("when not consistent MSG", func(t *testing.T) {
217217
message := &Message{mailfrom: true, rcptto: true, data: true}
218218

219-
assert.False(t, message.isConsistent())
219+
assert.False(t, message.IsConsistent())
220220
})
221221
}
222222

@@ -239,6 +239,31 @@ func TestMessagesAppend(t *testing.T) {
239239
message, messages := new(Message), new(messages)
240240
messages.append(message)
241241

242+
messages.RLock()
242243
assert.Same(t, message, messages.items[0])
244+
messages.RUnlock()
245+
})
246+
}
247+
248+
func TestMessagesCopy(t *testing.T) {
249+
t.Run("copies messages", func(t *testing.T) {
250+
message, messages := new(Message), new(messages)
251+
message.heloRequest = "foobar"
252+
messages.append(message)
253+
copyMessages := messages.copy()
254+
255+
assert.Len(t, copyMessages, 1)
256+
assert.Equal(t, *message, copyMessages[0])
257+
})
258+
}
259+
260+
func TestMessagesPurge(t *testing.T) {
261+
t.Run("purges messages from items slice", func(t *testing.T) {
262+
message, messages := new(Message), new(messages)
263+
messages.append(message)
264+
265+
assert.Len(t, messages.copy(), 1)
266+
assert.Len(t, messages.purge(), 1)
267+
assert.Len(t, messages.copy(), 0)
243268
})
244269
}

server.go

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,14 @@ func (server *Server) Stop() (err error) {
121121
// Public interface to get access to server messages.
122122
// Returns slice with copy of messages
123123
func (server *Server) Messages() []Message {
124-
server.Lock()
125-
defer server.Unlock()
126-
copiedMessages, messages := []Message{}, server.messages.items
127-
for index := range messages {
128-
copiedMessages = append(copiedMessages, *messages[index])
129-
}
124+
return server.messages.copy()
125+
}
130126

131-
return copiedMessages
127+
// Public interface to get access to server messages
128+
// and at the same time removes them.
129+
// Returns slice with copy of messages
130+
func (server *Server) MessagesAndPurge() []Message {
131+
return server.messages.purge()
132132
}
133133

134134
// Thread-safe getter of server port.
@@ -175,19 +175,13 @@ func (server *Server) stop() {
175175
server.started = false
176176
}
177177

178-
// Creates and assigns new message to server.messages
179-
func (server *Server) newMessage() *Message {
180-
newMessage := new(Message)
181-
server.messages.append(newMessage)
182-
return newMessage
183-
}
184-
185178
// Creates and assigns new message with helo context from other message to server.messages
186179
func (server *Server) newMessageWithHeloContext(otherMessage *Message) *Message {
187-
newMessage := server.newMessage()
180+
newMessage := new(Message)
188181
newMessage.heloRequest = otherMessage.heloRequest
189182
newMessage.heloResponse = otherMessage.heloResponse
190183
newMessage.helo = otherMessage.helo
184+
server.messages.append(otherMessage)
191185
return newMessage
192186
}
193187

@@ -221,7 +215,10 @@ func (server *Server) isAbleToEndSession(message *Message, session sessionInterf
221215
//nolint:gocyclo // SMTP client-server session handler
222216
func (server *Server) handleSession(session sessionInterface) {
223217
defer session.finish()
224-
message, configuration := server.newMessage(), server.configuration
218+
message, configuration := new(Message), server.configuration
219+
defer func() {
220+
server.messages.append(message)
221+
}()
225222
session.writeResponse(configuration.msgGreeting, defaultSessionResponseDelay)
226223

227224
for {
@@ -244,7 +241,7 @@ func (server *Server) handleSession(session sessionInterface) {
244241
case "HELO", "EHLO":
245242
newHandlerHelo(session, message, configuration).run(request)
246243
case "MAIL":
247-
if configuration.multipleMessageReceiving && message.rset && message.isConsistent() {
244+
if configuration.multipleMessageReceiving && message.rset && message.IsConsistent() {
248245
message = server.newMessageWithHeloContext(message)
249246
}
250247

0 commit comments

Comments
 (0)