1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
diff --git a/README.md b/README.md
index bc382f43dba8b8e384fe2e2245075bb0799906be..a03ee3a88a6420aea2d1a219386ceb1630d32f5f 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ | CAPABILITIES | mandatory | ๐ธ | RFC3977 |
| HEAD | mandatory | ๐ธ | RFC3977 |
| HELP | mandatory | ๐ธ | RFC3977 |
| QUIT | mandatory | ๐ธ | RFC3977 |
-| STAT | mandatory | โ | RFC3977 |
+| STAT | mandatory | ๐ธ | RFC3977 |
| AUTHINFO USER | AUTHINFO USER | ๐ธ | RFC4643 |
| AUTHINFO PASS | AUTHINFO USER | ๐ธ | RFC4643 |
| AUTHINFO SASLยน | AUTHINFO SASL | โ | RFC4643 |
diff --git a/mandatory.go b/mandatory.go
index b0c685f17263675b899093f4f3e3b5c488438e9c..17530f3971467541703cafd59df6d3851a285329 100644
--- a/mandatory.go
+++ b/mandatory.go
@@ -53,3 +53,19 @@ default:
return "", ErrUnexpectedResponse
}
}
+
+func (c *Conn) Stat(ctx context.Context, msg MessageIdentifier) error {
+ resp, err := c.CmdNoBody(ctx, "STAT %s", msg.msgID())
+ if err != nil {
+ return fmt.Errorf("failed to retrieve header for article '%s': %w", msg.msgID(), err)
+ }
+
+ switch resp.Status.Code {
+ case StatusArticleExists:
+ return nil
+ case StatusNoArticleWithGivenMessageID:
+ return ErrNoArticleWithGivenMessageID
+ default:
+ return ErrUnexpectedResponse
+ }
+}
diff --git a/mandatory_test.go b/mandatory_test.go
index 6d96493b318c20c44b8ca3117bac9d406752483f..f4d5626c522d6ab4ea752a2267aa328b4ffffc48 100644
--- a/mandatory_test.go
+++ b/mandatory_test.go
@@ -154,3 +154,89 @@ t.Errorf("mandatory command '%s' is missing", key)
}
}
}
+
+func TestStat(t *testing.T) {
+ if NewsServerSecure == "" || NewsServerUser == "" || NewsServerPassword == "" {
+ t.Log("secure server address, username, and password required in variables_test.go")
+ t.SkipNow()
+ }
+
+ tests := []struct {
+ name string
+ message nntp.MessageIdentifier
+ expectedError error
+ preCommand func(*nntp.Conn) error
+ }{
+ /*{ // Eweka send 430 instead
+ name: "no-group-selected",
+ message: nntp.CurrentMessage,
+ expectedError: nntp.ErrNoNewsgroupSelected,
+ preCommand: func(c *nntp.Conn) error {
+ return c.Group("alt.binaries.test")
+ },
+ },*/
+ {
+ name: "article-number",
+ message: nntp.ArticleNumber(5666767574),
+ expectedError: nil,
+ preCommand: func(c *nntp.Conn) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ return c.Group(ctx, "alt.binaries.test")
+ },
+ },
+ /*{ // Eweka sends 430 instead
+ name: "invalid-number",
+ message: nntp.ArticleNumber(2),
+ expectedError: nntp.ErrNoArticleWithGivenNumber,
+ preCommand: func(c *nntp.Conn) error {
+ return c.Group("alt.binaries.test")
+ },
+ },*/
+ {
+ name: "invalid-id",
+ message: nntp.MessageID("billy@bub"),
+ expectedError: nntp.ErrNoArticleWithGivenMessageID,
+ preCommand: func(c *nntp.Conn) error {
+ return nil
+ },
+ },
+ {
+ name: "message-id",
+ message: nntp.MessageID("64dae59a2ab54b328181a29e35462fb5@ngPost"),
+ expectedError: nil,
+ preCommand: func(c *nntp.Conn) error {
+ return nil
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ c, err := nntp.Dial(NewsServerPlain, nntp.OptionUnencrypted)
+ if err != nil {
+ t.Skipf("connection to '%s' failed: %v", NewsServerSecure, err)
+ }
+ defer c.Close(context.Background())
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ err = c.LoginUserPass(ctx, NewsServerUser, NewsServerPassword)
+ if err != nil {
+ t.Skipf("failed to authenticate: %v", err)
+ }
+
+ err = test.preCommand(c)
+ if err != nil {
+ t.Skipf("failed preparation: %v", err)
+ }
+
+ ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ err = c.Stat(ctx, test.message)
+ if !errors.Is(err, test.expectedError) {
+ t.Errorf("unexpected result:\n\tgot: %v\n\texpected: %v\n", err, test.expectedError)
+ }
+ })
+ }
+}
diff --git a/status.go b/status.go
index fb97f19682c7a797acf0abccc99e8129794f9dc8..2f76695780b5e08f9b0ba874e5abdbed9bb6d0e5 100644
--- a/status.go
+++ b/status.go
@@ -9,6 +9,7 @@ StatusServiceNoPosting = 201
StatusConnectionClosing = 205
StatusGroupSelected = 211
StatusHeadersFollow = 221
+ StatusArticleExists = 223
StatusAuthAccepted = 281
StatusAuthAcceptedWithData = 283
StatusAuthPassRequired = 381
|