-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclient.ts
2751 lines (2675 loc) · 103 KB
/
client.ts
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { BufReader, log, readerFromStreamReader } from "./deps.ts";
import {
Article,
Command,
LF,
Response,
TERMINATING_LINE,
TERMINATION,
} from "./mod.ts";
type parameter = string | number | undefined;
type wildmat = string;
export interface ConnectOptions extends Deno.ConnectOptions {
ssl?: boolean;
logLevel?: log.LevelName;
}
export interface NNTPClient {
capabilities(keyword?: string): Promise<Response>;
modeReader(): Promise<Response>;
quit(): Promise<Response>;
group(group?: string): Promise<Response>;
listgroup(group?: string, range?: string): Promise<Response>;
last(): Promise<Response>;
next(): Promise<Response>;
article(number?: number): Promise<Response>;
article(messageId?: string): Promise<Response>;
head(number?: number): Promise<Response>;
head(messageId?: string): Promise<Response>;
body(number?: number): Promise<Response>;
body(messageId?: string): Promise<Response>;
stat(number?: number): Promise<Response>;
stat(messageId?: string): Promise<Response>;
post(article: Article): Promise<Response>;
ihave(messageId: string, article: Article): Promise<Response>;
date(): Promise<Response>;
help(): Promise<Response>;
newgroups(date: string, time: string, isGMT?: boolean): Promise<Response>;
newnews(
wildmat: wildmat,
date: string,
time: string,
isGMT?: boolean,
): Promise<Response>;
list(keyword?: string, arg?: wildmat | parameter): Promise<Response>;
over(messageId?: string): Promise<Response>;
over(range?: string): Promise<Response>;
over(arg?: string): Promise<Response>;
hdr(field: string, messageId?: string): Promise<Response>;
hdr(field: string, range?: string): Promise<Response>;
hdr(field: string, arg?: string): Promise<Response>;
authinfo(username: string, password?: string): Promise<Response>;
}
/**
* Client to communite with NNTP server to send command and handle response.
*
* ```ts
* import { Client } from "./client.ts";
*
* const client: Client = new Client({ hostname: "127.0.0.1", port: 119 });
* await client.connect();
* const response: Response = await client.capabilities();
* console.log({
* status: response.status,
* statusText: response.statusText,
* headers: response.headers,
* body: await response.text(),
* });
* await client.quit();
* ```
*/
export class Client implements NNTPClient {
#options: ConnectOptions;
#connection?: Deno.TcpConn | Deno.TlsConn;
#logger?: log.Logger;
#authenticated = false;
/**
* Creates a Client and connects to NNTP server and returns its greeting.
*
* ## Examples
*
* ```ts
* import { Client } from "./client.ts";
* const client: Client = await Client.connect({ hostname: "127.0.0.1", port: 119 });
* ```
*/
static async connect(options?: ConnectOptions) {
const client = new Client(options);
await client.connect();
return client;
}
constructor(options?: ConnectOptions) {
options = {
port: 119,
logLevel: "INFO",
...options || {},
};
this.#options = options;
}
get authenticated() {
return this.#authenticated;
}
/**
* Connects to NNTP server and returns its greeting.
*
* ## Examples
*
* ```ts
* import { Client } from "./client.ts";
* const client = new Client({ hostname: "127.0.0.1", port: 119 });
* const response = await client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* console.assert(response.ok);
* ```
*/
async connect(options: ConnectOptions = this.#options): Promise<Response> {
const {
hostname,
port,
ssl,
logLevel,
} = options;
await log.setup({
handlers: {
console: new log.handlers.ConsoleHandler(logLevel!),
},
loggers: {
nntp: {
level: logLevel,
handlers: ["console"],
},
},
});
this.#logger = log.getLogger("nntp");
if (ssl) {
this.#connection = await Deno.connectTls({ hostname, port });
} else {
this.#connection = await Deno.connect({ hostname, port });
}
// When the connection is established, the NNTP server host
// MUST send a greeting.
return this.#getResponse();
}
async #getResponse(): Promise<Response> {
const response: Response = await Response.from(
this.#connection as Deno.Reader,
);
const {
status,
statusText,
headers,
} = response;
const log = this.#logger!;
log.info(`[S] ${status} ${statusText}`);
for (const header of headers.entries()) {
log.info(`[S] ${header[0]}: ${header[1].replace(/\r?\n|\r/, "")}`);
}
// Logs body if required.
const body = response.body?.pipeThrough(
new TransformStream({
transform(chunk, controller) {
log.debug(() => {
const msg = new TextDecoder().decode(chunk).replace(/\r?\n|\r/, "");
return `[S] ${msg}`;
});
controller.enqueue(chunk);
},
}),
);
return new Response(body, {
status,
statusText,
headers,
});
}
/**
* Sends a NNTP command to the server.
*
* Commands in NNTP MUST consist of a keyword, which MAY be followed by one.
* or more arguments. A CRLF pair MUST terminate all commands. Multiple
* commands MUST NOT be on the same line.
*
* Command lines MUST NOT exceed 512 octets, which includes the terminating
* CRLF pair. The arguments MUST NOT exceed 497 octets.
*
* ## Examples
*
* ```ts
* import { Client } from "./client.ts";
* const client = new Client();
* await client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* const response = await client.request("GROUP", "misc.test")
* // [C] GROUP misc.test
* // [S] 211 1234 3000234 3002322 misc.test
* console.assert(response.status === 211)
* ```
*/
async request(command: string, ...args: parameter[]): Promise<Response>;
async request(command: Command, ...args: parameter[]): Promise<Response>;
async request(
stream: ReadableStream,
...args: parameter[]
): Promise<Response>;
async request(
input: Command | string | ReadableStream,
...args: parameter[]
): Promise<Response> {
const writer = this.#connection!;
if (typeof input === "string") {
input = input.toUpperCase() as Command;
const line = [input, ...args.map(normalize)].join(" ");
// Obfucastes password in log.
if (input === "AUTHINFO PASS") {
args[0] = (args[0]! as string).replace(/./g, "*");
}
this.#logger!.info(`[C] ${[input, ...args.map(normalize)].join(" ")}`);
await writer.write(new TextEncoder().encode(`${line}\r\n`));
} else {
const reader = readerFromStreamReader(input.getReader());
// Uses a BufReader to read line by line.
const bufReader = BufReader.create(reader);
let gotEOF = false, bytesWritten = 0;
while (gotEOF === false) {
const line = (await bufReader.readSlice(LF))?.slice() ||
new Uint8Array();
if (line.every((value, index) => value === TERMINATING_LINE[index])) {
await writer.write(line);
gotEOF = true;
break;
}
// Dot-stuffs the line with another TERMINATION before sending.
if (line[0] === TERMINATION) {
bytesWritten += await writer.write(
Uint8Array.from([TERMINATION, ...line]),
);
} else {
bytesWritten += await writer.write(line);
}
}
}
return this.#getResponse();
}
close() {
this.#authenticated = false;
this.#connection?.close();
}
//#region 5. Session Administration Commands
/**
* The CAPABILITIES command allows a client to determine the
* capabilities of the server at any given time.
*
* This command MAY be issued at any time; the server MUST NOT require
* it to be issued in order to make use of any capability. The response
* generated by this command MAY change during a session because of
* other state information (which, in turn, may be changed by the
* effects of other commands or by external events). An NNTP client is
* only able to get the current and correct information concerning
* available capabilities at any point during a session by issuing a
* CAPABILITIES command at that point of that session and processing the
* response.
*
* The capability list is returned as a multi-line data block following
* the 101 response code. Each capability is described by a separate
* capability line. The server MUST NOT list the same capability twice
* in the response, even with different arguments. Except that the
* VERSION capability MUST be the first line, the order in which the
* capability lines appears is not significant; the server need not even
* consistently return the same order.
*
* While some capabilities are likely to be always available or never
* available, others (notably extensions) will appear and disappear
* depending on server state changes within the session or on external
* events between sessions. An NNTP client MAY cache the results of
* this command, but MUST NOT rely on the correctness of any cached
* results, whether from earlier in this session or from a previous
* session, MUST cope gracefully with the cached status being out of
* date, and SHOULD (if caching results) provide a way to force the
* cached information to be refreshed. Furthermore, a client MUST NOT
* use cached results in relation to security, privacy, and
* authentication extensions.
*
* The keyword argument is not used by this specification. It is
* provided so that extensions or revisions to this specification can
* include extra features for this command without requiring the
* CAPABILITIES command to be used twice (once to determine if the extra
* features are available, and a second time to make use of them). If
* the server does not recognise the argument (and it is a keyword), it
* MUST respond with the 101 response code as if the argument had been
* omitted. If an argument is provided that the server does recognise,
* it MAY use the 101 response code or MAY use some other response code
* (which will be defined in the specification of that feature). If the
* argument is not a keyword, the 501 generic response code MUST be
* returned. The server MUST NOT generate any other response code to
* the CAPABILITIES command.
*
* ## Examples
*
* Example of a minimal response (a read-only server):
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* const response = await client.capabilities();
* // [C] CAPABILITIES
* // [S] 101 Capability list:
* // [S] VERSION 2
* // [S] READER
* // [S] LIST ACTIVE NEWSGROUPS
* // [S] .
* ```
*/
capabilities(keyword?: string): Promise<Response> {
return this.request(Command.CAPABILITIES, keyword);
}
/**
* The MODE READER command instructs a mode-switching server to switch
* modes.
*
* If the server is mode-switching, it switches from its transit mode to
* its reader mode, indicating this by changing the capability list
* accordingly. It MUST then return a 200 or 201 response with the same
* meaning as for the initial greeting (as described in Section 5.1.1).
* Note that the response need not be the same as that presented during
* the initial greeting. The client MUST NOT issue MODE READER more
* than once in a session or after any security or privacy commands are
* issued. When the MODE READER command is issued, the server MAY reset
* its state to that immediately after the initial connection before
* switching mode.
* If the server is not mode-switching, then the following apply:
* - If it advertises the READER capability, it MUST return a 200 or
* 201 response with the same meaning as for the initial greeting; in
* this case, the command MUST NOT affect the server state in any
* way.
* - If it does not advertise the READER capability, it MUST return a
* 502 response and then immediately close the connection.
*
* ## Examples
*
* Example of use of the MODE READER command on a transit-only server
* (which therefore does not providing reading facilities):
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* let response = await client.capabilities();
* // [C] CAPABILITIES
* // [S] 101 Capability list:
* // [S] VERSION 2
* // [S] IHAVE
* // [S] .
* response = await client.modeReader();
* // [C] MODE READER
* // [S] 502 Transit service only
* ```
*/
modeReader(): Promise<Response> {
return this.request(Command["MODE READER"]);
}
/**
* The client uses the QUIT command to terminate the session.
*
* The server MUST acknowledge the QUIT command and then close the
* connection to the client. This is the preferred method for a client
* to indicate that it has finished all of its transactions with the
* NNTP server.
*
* ## Examples
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.quit();
* // [C] QUIT
* // [S] 205 closing connection
* ```
*/
async quit(): Promise<Response> {
const response = await this.request(Command.QUIT);
this.#authenticated = false;
this.#connection?.close();
return response;
}
//#endregion 5. Session Administration Commands
//#region 6. Article Posting and Retrieval
//#region 6.1 Group and Article Selection
/**
* The GROUP command selects a newsgroup as the currently selected
* newsgroup and returns summary information about it.
*
* The required argument is the name of the newsgroup to be selected
* (e.g., "news.software.nntp"). A list of valid newsgroups may be
* obtained by using the LIST ACTIVE command.
*
* The successful selection response will return the article numbers of
* the first and last articles in the group at the moment of selection
* (these numbers are referred to as the "reported low water mark" and
* the "reported high water mark") and an estimate of the number of
* articles in the group currently available.
*
* If the group is not empty, the estimate MUST be at least the actual
* number of articles available and MUST be no greater than one more
* than the difference between the reported low and high water marks.
* (Some implementations will actually count the number of articles
* currently stored. Others will just subtract the low water mark from
* the high water mark and add one to get an estimate.)
*
* If the group is empty, one of the following three situations will
* occur. Clients MUST accept all three cases; servers MUST NOT
* represent an empty group in any other way.
*
* - The high water mark will be one less than the low water mark, and
* the estimated article count will be zero. Servers SHOULD use this
* method to show an empty group. This is the only time that the
* high water mark can be less than the low water mark.
* - All three numbers will be zero.
* - The high water mark is greater than or equal to the low water
* mark. The estimated article count might be zero or non-zero; if
* it is non-zero, the same requirements apply as for a non-empty
* group.
*
* The set of articles in a group may change after the GROUP command is
* carried out:
*
* - Articles may be removed from the group.
* - Articles may be reinstated in the group with the same article
* number, but those articles MUST have numbers no less than the
* reported low water mark (note that this is a reinstatement of the
* previous article, not a new article reusing the number).
* - New articles may be added with article numbers greater than the
* reported high water mark. (If an article that was the one with
* the highest number has been removed and the high water mark has
* been adjusted accordingly, the next new article will not have the
* number one greater than the reported high water mark.)
*
* Except when the group is empty and all three numbers are zero,
* whenever a subsequent GROUP command for the same newsgroup is issued,
* either by the same client or a different client, the reported low
* water mark in the response MUST be no less than that in any previous
* response for that newsgroup in this session, and it SHOULD be no less
* than that in any previous response for that newsgroup ever sent to
* any client. Any failure to meet the latter condition SHOULD be
* transient only. The client may make use of the low water mark to
* remove all remembered information about articles with lower numbers,
* as these will never recur. This includes the situation when the high
* water mark is one less than the low water mark. No similar
* assumption can be made about the high water mark, as this can
* decrease if an article is removed and then increase again if it is
* reinstated or if new articles arrive.
*
* When a valid group is selected by means of this command, the
* currently selected newsgroup MUST be set to that group, and the
* current article number MUST be set to the first article in the group
* (this applies even if the group is already the currently selected
* newsgroup). If an empty newsgroup is selected, the current article
* number is made invalid. If an invalid group is specified, the
* currently selected newsgroup and current article number MUST NOT be
* changed.
*
* The GROUP or LISTGROUP command (see Section 6.1.2) MUST be used by a
* client, and a successful response received, before any other command
* is used that depends on the value of the currently selected newsgroup
* or current article number.
*
* If the group specified is not available on the server, a 411 response
* MUST be returned.
*
* ## Examples
*
* Example for a group known to the server:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("misc.test");
* // [C] GROUP misc.test
* // [S] 211 1234 3000234 3002322 misc.test
* ```
*
* Example for a group unknown to the server:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("example.is.sob.bradner.or.barber");
* // [C] GROUP example.is.sob.bradner.or.barber
* // [S] 411 example.is.sob.bradner.or.barber is unknown
* ```
*
* Example of an empty group using the preferred response:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("example.currently.empty.newsgroup");
* // [C] GROUP example.currently.empty.newsgroup
* // [S] 211 0 4000 3999 example.currently.empty.newsgroup
* ```
*
* Example of an empty group using an alternative response:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("example.currently.empty.newsgroup");
* // [C] GROUP example.currently.empty.newsgroup
* // [S] 211 0 0 0 example.currently.empty.newsgroup
* ```
*
* Example of an empty group using a different alternative response:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("example.currently.empty.newsgroup");
* // [C] GROUP example.currently.empty.newsgroup
* // [S] 211 0 4000 4321 example.currently.empty.newsgroup
* ```
*
* Example reselecting the currently selected newsgroup:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("misc.test");
* // [C] GROUP misc.test
* // [S] 211 1234 234 567 misc.test
* await client.stat(444);
* // [C] STAT 444
* // [S] 223 444 <[email protected]> retrieved
* await client.group("misc.test");
* // [C] GROUP misc.test
* // [S] 211 1234 234 567 misc.test
* await client.stat();
* // [C] STAT
* // [S] 223 234 <[email protected]> retrieved
* ```
*/
group(group: string): Promise<Response> {
return this.request(Command.GROUP, group);
}
/**
* The LISTGROUP command selects a newsgroup in the same manner as the
* GROUP command (see Section 6.1.1) but also provides a list of article
* numbers in the newsgroup. If no group is specified, the currently
* selected newsgroup is used.
*
* On success, a list of article numbers is returned as a multi-line
* data block following the 211 response code (the arguments on the
* initial response line are the same as for the GROUP command). The
* list contains one number per line and is in numerical order. It
* lists precisely those articles that exist in the group at the moment
* of selection (therefore, an empty group produces an empty list). If
* the optional range argument is specified, only articles within the
* range are included in the list (therefore, the list MAY be empty even
* if the group is not).
*
* The range argument may be any of the following:
*
* - An article number.
* - An article number followed by a dash to indicate all following.
* - An article number followed by a dash followed by another article
* number.
*
* In the last case, if the second number is less than the first number,
* then the range contains no articles. Omitting the range is
* equivalent to the range 1- being specified.
*
* If the group specified is not available on the server, a 411 response
* MUST be returned. If no group is specified and the currently
* selected newsgroup is invalid, a 412 response MUST be returned.
*
* Except that the group argument is optional, that a range argument can
* be specified, and that a multi-line data block follows the 211
* response code, the LISTGROUP command is identical to the GROUP
* command. In particular, when successful, the command sets the
* current article number to the first article in the group, if any,
* even if this is not within the range specified by the second
* argument.
*
* Note that the range argument is a new feature in this specification
* and servers that do not support CAPABILITIES (and therefore do not
* conform to this specification) are unlikely to support it.
*
* ## Examples
*
* Example of LISTGROUP being used to select a group:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* const response = await client.listgroup("misc.test");
* // [C] LISTGROUP misc.test
* // [S] 211 2000 3000234 3002322 misc.test list follows
* // [S] 3000234
* // [S] 3000237
* // [S] 3000238
* // [S] 3000239
* // [S] 3002322
* // [S] .
* ```
*
* Example of LISTGROUP on an empty group:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* const response = await client.listgroup("example.empty.newsgroup");
* // [C] LISTGROUP example.empty.newsgroup
* // [S] 211 0 0 0 example.empty.newsgroup list follows
* // [S] .
* ```
*
* Example of LISTGROUP on a valid, currently selected newsgroup:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("misc.test");
* // [C] GROUP misc.test
* // [S] 211 2000 3000234 3002322 misc.test
* const response = await client.listgroup();
* // [C] LISTGROUP
* // [S] 211 2000 3000234 3002322 misc.test list follows
* // [S] 3000234
* // [S] 3000237
* // [S] 3000238
* // [S] 3000239
* // [S] 3002322
* // [S] .
* ```
*
* Example of LISTGROUP with a range:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* const response = await client.listgroup("misc.test", "3000238-3000248");
* // [C] LISTGROUP misc.test 3000238-3000248
* // [S] 211 2000 3000234 3002322 misc.test list follows
* // [S] 3000238
* // [S] 3000239
* // [S] .
* ```
*
* Example of LISTGROUP with an empty range:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* const response = await client.listgroup("misc.test", "12345678-");
* // [C] LISTGROUP misc.test 12345678-
* // [S] 211 2000 3000234 3002322 misc.test list follows
* // [S] .
* ```
*
* Example of LISTGROUP with an invalid range:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* const response = await client.listgroup("misc.test", "9999-111");
* // [C] LISTGROUP misc.test 9999-111
* // [S] 211 2000 3000234 3002322 misc.test list follows
* // [S] .
* ```
*/
listgroup(group?: string, range?: string): Promise<Response> {
return this.request(Command.LISTGROUP, group, range);
}
/**
* If the currently selected newsgroup is valid, the current article
* number MUST be set to the previous article in that newsgroup (that
* is, the highest existing article number less than the current article
* number). If successful, a response indicating the new current
* article number and the message-id of that article MUST be returned.
* No article text is sent in response to this command.
*
* There MAY be no previous article in the group, although the current
* article number is not the reported low water mark. There MUST NOT be
* a previous article when the current article number is the reported
* low water mark.
*
* Because articles can be removed and added, the results of multiple
* LAST and NEXT commands MAY not be consistent over the life of a
* particular NNTP session.
*
* If the current article number is already the first article of the
* newsgroup, a 422 response MUST be returned. If the current article
* number is invalid, a 420 response MUST be returned. If the currently
* selected newsgroup is invalid, a 412 response MUST be returned. In
* all three cases, the currently selected newsgroup and current article
* number MUST NOT be altered.
*
* ## Examples
*
* Example of a successful article retrieval using LAST:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("misc.test");
* // [C] GROUP misc.test
* // [S] 211 1234 3000234 3002322 misc.test
* await client.next();
* // [C] NEXT
* // [S] 223 3000237 <[email protected]> retrieved
* await client.last();
* // [C] LAST
* // [S] 223 3000234 <[email protected]> retrieved
* ```
*
* Example of an attempt to retrieve an article without having selected
* a group (via the GROUP command) first:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.last();
* // [C] LAST
* // [S] 412 no newsgroup selected
* ```
*
* Example of an attempt to retrieve an article using the LAST command
* when the current article number is that of the first article in the
* group:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("misc.test");
* // [C] GROUP misc.test
* // [S] 211 1234 3000234 3002322 misc.test
* await client.last();
* // [C] LAST
* // [S] 422 No previous article to retrieve
* ```
*
* Example of an attempt to retrieve an article using the LAST command
* when the currently selected newsgroup is empty:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("example.empty.newsgroup");
* // [C] GROUP example.empty.newsgroup
* // [S] 211 0 0 0 example.empty.newsgroup
* await client.last();
* // [C] LAST
* // [S] 420 No current article selected
*/
last(): Promise<Response> {
return this.request(Command.LAST);
}
/**
* If the currently selected newsgroup is valid, the current article
* number MUST be set to the next article in that newsgroup (that is,
* the lowest existing article number greater than the current article
* number). If successful, a response indicating the new current
* article number and the message-id of that article MUST be returned.
* No article text is sent in response to this command.
*
* If the current article number is already the last article of the
* newsgroup, a 421 response MUST be returned. In all other aspects
* (apart, of course, from the lack of 422 response), this command is
* identical to the LAST command (Section 6.1.3).
*
* ## Examples
*
* Example of a successful article retrieval using NEXT:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("misc.test");
* // [C] GROUP misc.test
* // [S] 211 1234 3000234 3002322 misc.test
* await client.next();
* // [C] NEXT
* // [S] 223 3000237 <[email protected]> retrieved
* ```
*
* Example of an attempt to retrieve an article without having selected
* a group (via the GROUP command) first:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.next();
* // [C] NEXT
* // [S] 412 no newsgroup selected
* ```
*
* Example of an attempt to retrieve an article using the NEXT command
* when the current article number is that of the last article in the
* group:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("example.empty.newsgroup");
* // [C] GROUP example.empty.newsgroup
* // [S] 211 0 0 0 example.empty.newsgroup
* await client.next();
* // [C] NEXT
* // [S] 420 No current article selected
* ```
*/
next(): Promise<Response> {
return this.request(Command.NEXT);
}
//#endregion 6.1 Group and Article Selection
//#region 6.2 Retrieval of Articles and Article Sections
/**
* The ARTICLE command selects an article according to the arguments and
* presents the entire article (that is, the headers, an empty line, and
* the body, in that order) to the client. The command has three forms.
*
* In the first form, a message-id is specified, and the server presents
* the article with that message-id. In this case, the server MUST NOT
* alter the currently selected newsgroup or current article number.
* This is both to facilitate the presentation of articles that may be
* referenced within another article being read, and because of the
* semantic difficulties of determining the proper sequence and
* membership of an article that may have been cross-posted to more than
* one newsgroup.
*
* In the response, the article number MUST be replaced with zero,
* unless there is a currently selected newsgroup and the article is
* present in that group, in which case the server MAY use the article's
* number in that group. (The server is not required to determine
* whether the article is in the currently selected newsgroup or, if so,
* what article number it has; the client MUST always be prepared for
* zero to be specified.) The server MUST NOT provide an article number
* unless use of that number in a second ARTICLE command immediately
* following this one would return the same article. Even if the server
* chooses to return article numbers in these circumstances, it need not
* do so consistently; it MAY return zero to any such command (also see
* the STAT examples, Section 6.2.4.3).
*
* In the second form, an article number is specified. If there is an
* article with that number in the currently selected newsgroup, the
* server MUST set the current article number to that number.
*
* In the third form, the article indicated by the current article
* number in the currently selected newsgroup is used.
*
* Note that a previously valid article number MAY become invalid if the
* article has been removed. A previously invalid article number MAY
* become valid if the article has been reinstated, but this article
* number MUST be no less than the reported low water mark for that
* group.
*
* The server MUST NOT change the currently selected newsgroup as a
* result of this command. The server MUST NOT change the current
* article number except when an article number argument was provided
* and the article exists; in particular, it MUST NOT change it
* following an unsuccessful response.
*
* Since the message-id is unique for each article, it may be used by a
* client to skip duplicate displays of articles that have been posted
* more than once, or to more than one newsgroup.
*
* The article is returned as a multi-line data block following the 220
* response code.
*
* If the argument is a message-id and no such article exists, a 430
* response MUST be returned. If the argument is a number or is omitted
* and the currently selected newsgroup is invalid, a 412 response MUST
* be returned. If the argument is a number and that article does not
* exist in the currently selected newsgroup, a 423 response MUST be
* returned. If the argument is omitted and the current article number
* is invalid, a 420 response MUST be returned.
*
* ## Examples
*
* Example of a successful retrieval of an article (explicitly not using
* an article number):
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.group("misc.test");
* // [C] GROUP misc.test
* // [S] 211 1234 3000234 3002322 misc.test
* await client.article();
* // [C] ARTICLE
* // [S] 220 3000234 <[email protected]>
* // [S] Path: pathost!demo!whitehouse!not-for-mail
* // [S] From: "Demo User" <[email protected]>
* // [S] Newsgroups: misc.test
* // [S] Subject: I am just a test article
* // [S] Date: 6 Oct 1998 04:38:40 -0500
* // [S] Organization: An Example Net, Uncertain, Texas
* // [S] Message-ID: <[email protected]>
* // [S]
* // [S] This is just a test article.
* // [S] .
* ```
*
* Example of a successful retrieval of an article by message-id:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.article("<[email protected]>");
* // [C] ARTICLE <[email protected]>
* // [S] 220 0 <[email protected]>
* // [S] Path: pathost!demo!whitehouse!not-for-mail
* // [S] From: "Demo User" <[email protected]>
* // [S] Newsgroups: misc.test
* // [S] Subject: I am just a test article
* // [S] Date: 6 Oct 1998 04:38:40 -0500
* // [S] Organization: An Example Net, Uncertain, Texas
* // [S] Message-ID: <[email protected]>
* // [S]
* // [S] This is just a test article.
* // [S] .
* ```
*
* Example of an unsuccessful retrieval of an article by message-id:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted
* await client.article("<[email protected]>");
* // [C] ARTICLE <[email protected]>
* // [S] 430 No Such Article Found
* ```
*
* Example of an unsuccessful retrieval of an article by number:
*
* ```ts
* import { Client } from "./client.ts";
* const client = await Client.connect();
* // [S] 200 NNTP Service Ready, posting permitted