git.fiddlerwoaroof.com
Browse code

Initial import from tarball at http://znain.net/cl-nntp/

Brian O'Reilly authored on 25/01/2013 22:25:05
Showing 8 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,3 @@
1
+*.fasl
2
+*.pid
3
+*.log
0 4
new file mode 100644
... ...
@@ -0,0 +1,13 @@
1
+
2
+
3
+all : dist/cl-nntp.tar.gz
4
+
5
+dist/cl-nntp.tar.gz : *.lisp *.asd | dist
6
+	tar cfz dist/cl-nntp.tar.gz -C .. cl-nntp --exclude=.git* --exclude=dist
7
+
8
+dist :
9
+	mkdir dist
10
+
11
+.PHONY : clean
12
+clean :
13
+	rm -fr dist
... ...
@@ -1,4 +1,74 @@
1
-cl-nntp
2
-=======
3 1
 
4
-'( :author "Dimitre Liotev <dl@liotev.com>"   :version "1.0"   :maintainer "Dimitre Liotev <dl@liotev.com>"   :licence "GPL")
5 2
\ No newline at end of file
3
+Common Lisp NNTP client library
4
+===============================
5
+
6
+## Description
7
+
8
+This is a NNTP client library. The current implementation is not
9
+complete, posting is not implemented yet.
10
+
11
+To load the system:
12
+
13
+    (asdf:load-system "cl-nntp")
14
+
15
+Note that the name of the ASDF system is *``cl-nttp``*, but the lisp
16
+package is *``com.liotev.nntp``*, with *``cl-nntp``* and *``nntp``* specified
17
+as its nicknames.
18
+
19
+Here is an example of using some of the functions:
20
+
21
+    ;; Connect to an nntp server
22
+    (cl-nntp:connect "nntp.aioe.org" 119)
23
+
24
+    ;; Select a group
25
+    cl-nntp:group "comp.lang.lisp")
26
+
27
+    ;; Fetch the current article
28
+    (cl-nntp:article)
29
+
30
+The last server you connect to is the default server. If you connect to
31
+port 563 or 443, a TLS(SSL) connection will be used. If you want to use
32
+TLS with other ports you have to send the key parameter :use-tls to the
33
+cl-nntp:connect function. (Note that STARTTLS is not implemented yet but
34
+will be in the near future.)
35
+
36
+If the server requires authorization the use name and password will be
37
+searched in the user's .authinfo file, whose location you can specify by
38
+setting variable *authinfo-file-name*. There user name and password for
39
+the nntp server should be specified in the .authinfo file like this:
40
+
41
+    machine server-name login user-name password pass-word
42
+
43
+For example if the server name is nntp.giganews.com, the use name is
44
+xxxer and the password is zzz555 the line would look like this:
45
+
46
+    machine nntp.giganews.com login xxxer password zzz555
47
+
48
+If a authentication through the authinfo file does not succeed, the user
49
+will be asked to supply user name and password.
50
+
51
+
52
+## The Network News Transfer Protocol (NNTP)
53
+
54
+NNTP is a protocol for reading and posting Usenet articles [\[1\]](#links).
55
+It is described in several RFC's:
56
+
57
+ * [RFC 6048](http://www.rfc-editor.org/rfc/rfc6048.txt) - Network News
58
+   Transfer Protocol (NNTP) Additions to LIST Command
59
+
60
+ * [RFC 4643](http://www.rfc-editor.org/rfc/rfc4643.txt) - Network News
61
+   Transfer Protocol (NNTP) - Extension for Authentication
62
+
63
+ * [RFC 4642](http://www.rfc-editor.org/rfc/rfc4642.txt) - Using
64
+   Transport Layer Security (TLS) with Network News Transfer Protocol
65
+   (NNTP)
66
+
67
+ * [RFC 3977](http://www.rfc-editor.org/rfc/rfc3977.txt) - Network News
68
+   Transfer Protocol (NNTP)
69
+
70
+ * [RFC 2980](http://www.rfc-editor.org/rfc/rfc2980.txt) - Common NNTP
71
+   Extensions
72
+
73
+## Links
74
+<a name="links"/>
75
+1. [The NNTP protocol](http://en.wikipedia.org/wiki/Network_News_Transfer_Protocol)
6 76
new file mode 100644
... ...
@@ -0,0 +1,19 @@
1
+;; -*- Mode: LISP; Syntax: COMMON-LISP; Package: CL-USER; Base: 10 -*-
2
+
3
+(in-package #:cl-user)
4
+
5
+(defpackage :cl-nntp-system
6
+  (:use #:asdf #:cl))
7
+
8
+(in-package #:cl-nntp-system)
9
+
10
+(defsystem :cl-nntp
11
+  :author "Dimitre Liotev <dl@liotev.com>"
12
+  :version "1.0"
13
+  :maintainer "Dimitre Liotev <dl@liotev.com>"
14
+  :licence "GPL"
15
+  :description "An NNTP client library"
16
+  :components ((:file "packages")
17
+               (:file "utils" :depends-on ("packages"))
18
+               (:file "nntp" :depends-on ("packages" "utils")))
19
+  :depends-on (#:usocket #:split-sequence #:cl+ssl #:babel))
0 20
new file mode 100644
... ...
@@ -0,0 +1,20 @@
1
+
2
+;; This an example of fetching an NNTP article. The server nntp.aioe.org
3
+;; is free and does not require any registration, so you should have no
4
+;; problems connecting to it. If you do get a connection error try
5
+;; another nntp server.
6
+
7
+;; To load with SBCL:
8
+;; sbcl --load example.lisp
9
+
10
+;; To load with ClozureCL (CCL):
11
+;; ccl --load example.lisp
12
+
13
+(asdf:load-system "cl-nntp")
14
+
15
+(cl-nntp:connect "nntp.aioe.org" 119)
16
+(cl-nntp:group "comp.lang.lisp")
17
+(cl-nntp:next-article)
18
+(let ((article (cl-nntp:article)))
19
+  ;; write the article to standard output
20
+  (write article))
0 21
new file mode 100644
... ...
@@ -0,0 +1,550 @@
1
+(in-package #:com.liotev.nntp)
2
+
3
+(defparameter *default-client* nil
4
+  "This client will be used in all functions that accept a client as an
5
+optional parameter, and this optional parameter is not supplied or
6
+nil.")
7
+
8
+(defparameter *cache-authinfo-p* nil
9
+  "If t, the user name and password will be saved in the client
10
+object.")
11
+
12
+(defparameter *authinfo-file-name*
13
+  (str (user-homedir-pathname)
14
+       ".authinfo")
15
+  "The authinfo file name.")
16
+
17
+(defparameter *reconnect-on-timeout* t
18
+  "Controls if reconnecting is attempted after receiving timeout message
19
+form the server.")
20
+
21
+(defparameter *reconnect-on-error* t
22
+  "Controls if reconnecting is attempted after an error.")
23
+
24
+(defparameter *use-tls* nil
25
+  "Controls if TLS is used for connecting to the server.")
26
+
27
+(defparameter *authinfo-config-function*
28
+  (list 'read-authinfo-file 'ask-autinfo-from-user)
29
+  "Function used to read the authinfo configuration.")
30
+
31
+(defparameter *clients* nil
32
+  "A list to hold all clients.")
33
+
34
+(defstruct client
35
+  "An nntp client."
36
+  host port last-command status-code status-message
37
+  stream greating user pass group
38
+  article-id article-number tls-p)
39
+
40
+(define-condition client-timeout (error)
41
+  ((client :initarg :client
42
+           :reader client))
43
+  (:documentation
44
+   "A condition to be signaled when the NNTP server sends
45
+a timeout message.")
46
+  (:report
47
+   (lambda (condition stream)
48
+     (let ((user (client-user (client condition)))
49
+           (host (client-host (client condition))))
50
+       (format stream
51
+               "Client ~A got timeout message from server ~A~%"
52
+               (str user "@" host) host)))))
53
+
54
+(defun open-stream-to-server
55
+    (host port &key (use-tls nil use-tls-supplied?))
56
+  (let ((stream (usocket:socket-stream
57
+                 (usocket:socket-connect host port))))
58
+    (if use-tls-supplied?
59
+        (if use-tls (cl+ssl:make-ssl-client-stream stream)
60
+            (values stream nil))
61
+        (if (or (string= (str port) "563")
62
+                (string= (str port) "443")
63
+                *use-tls*)
64
+            (values (cl+ssl:make-ssl-client-stream stream) t)
65
+            (values stream nil)))))
66
+
67
+(defun connect (host port &optional client
68
+                &key auth (use-tls nil use-tls-supplied?))
69
+  "Connects to the host. If a client is supplied just reconnects the
70
+client."
71
+  (format t "-> Connecting to ~a:~a~%" host port)
72
+  (multiple-value-bind (stream tls-p)
73
+      (if use-tls-supplied?
74
+          (open-stream-to-server host port :use-tls use-tls)
75
+          (open-stream-to-server host port))
76
+    (let ((server-greeting (if tls-p (read-line-tls stream)
77
+                               (read-line stream))))
78
+      (format t "<- ~a~%" server-greeting)
79
+      (force-output)
80
+      (if (check-status-code (subseq server-greeting 0 3) '("200" "201"))
81
+          (if (null client)
82
+              (let ((new-client
83
+                     (make-new-client host port stream server-greeting
84
+                                      tls-p)))
85
+                (register-client new-client)
86
+                (setf *default-client* new-client)
87
+                (when auth (authenticate-client new-client)))
88
+              (progn
89
+                (reset-client client host port stream server-greeting
90
+                              tls-p)
91
+                (when auth (authenticate-client client))))
92
+          (error "Invalid server response: ~a~%" server-greeting)))))
93
+
94
+(defun authenticate-client (client)
95
+  (dolist (func *authinfo-config-function*)
96
+    (let ((user-and-pass (funcall func (client-host client))))
97
+      (when user-and-pass
98
+        (apply #'authinfo
99
+               (append user-and-pass
100
+                       (list client)))
101
+        (return client)))))
102
+
103
+(defun make-new-client (host port stream server-greeting tls-p)
104
+  (make-client
105
+   :host host
106
+   :port port
107
+   :stream stream
108
+   :status-message server-greeting
109
+   :greating server-greeting
110
+   :status-code (subseq server-greeting 0 3)
111
+   :tls-p tls-p))
112
+
113
+(defun reset-client (client host port stream server-greeting tls-p)
114
+  (setf (client-host client) host)
115
+  (setf (client-port client) port)
116
+  (setf (client-stream client) stream)
117
+  (setf (client-greating client) server-greeting)
118
+  (setf (client-status-code client) (subseq server-greeting 0 3))
119
+  (setf (client-tls-p client) tls-p)
120
+  client)
121
+
122
+
123
+(defun do-command
124
+    (command &optional (client *default-client*)
125
+     &key (reconnect t reconnect-supplied-p))
126
+  "Sends the command to the server. In case of a timeout message from
127
+the server, a reconnect will be attempted in the following cases:
128
+
129
+   * the 'reconnect' key is supplied and it is t
130
+
131
+   * the 'reconnect' key is not supplied and *reconnect-on-timeout* is t
132
+
133
+In case of another error, a reconnect will be attempted if
134
+*reconnect-on-error* is t.
135
+"
136
+  (if (or (and reconnect-supplied-p reconnect)
137
+          (and (not reconnect-supplied-p) *reconnect-on-timeout*))
138
+      (handler-bind ((client-timeout #'reconnect)
139
+                     (error #'(lambda (c)
140
+                                (when *reconnect-on-error*
141
+                                  (reconnect c)))))
142
+        (send-command command client))
143
+      (send-command command client)))
144
+
145
+(defun reconnect (condition)
146
+  "Function for the 'rconnect' restart"
147
+  (format t "A condition of type ~a was signaled: ~A~%"
148
+          (type-of condition) condition)
149
+  (force-output)
150
+  (when (find-restart 'reconnect)
151
+    (invoke-restart 'reconnect)) )
152
+
153
+(defun send-command (command  &optional (client *default-client*))
154
+  "Sends the command to the server."
155
+  (setf (client-last-command client) command)
156
+  (restart-case
157
+      (progn (client-write-line command client)
158
+             (when (string= (get-status client) "480")
159
+               (authenticate-client client)
160
+               (client-write-line command client)
161
+               (get-status client))
162
+             (when (timeout? client)
163
+               (error 'client-timeout :client client))
164
+             (values (client-status-code client)
165
+                     (client-status-message client)))
166
+    (reconnect ()
167
+      (format t "Reconnecting to server ~A~%" (client-host client))
168
+      (force-output)
169
+      (reconnect-client client)
170
+      (let ((*reconnect-on-error* nil))
171
+        (send-command command client)))))
172
+
173
+(defun timeout? (client)
174
+  (when (and (string= "503" (client-status-code client))
175
+             (or (search "time out" (client-status-message client))
176
+                 (search "timeout" (client-status-message client))))
177
+    t))
178
+
179
+
180
+(defun group (group-name &optional (client *default-client*))
181
+  "The GROUP nntp command.
182
+Syntax
183
+  GROUP group
184
+
185
+Responses
186
+  211 number low high group  Group successfully selected
187
+  411                        No such newsgroup
188
+
189
+Parameters
190
+  group     Name of newsgroup
191
+  number    Estimated number of articles in the group
192
+  low       Reported low water mark
193
+  high      Reported high water mark
194
+"
195
+  (setf (client-group client) group-name)
196
+  (do-command (str "group " group-name) client))
197
+
198
+(defun article (&optional (client *default-client*)
199
+                &key article-number article-id)
200
+  "Retreives an article.
201
+Syntax
202
+  ARTICLE message-id
203
+  ARTICLE number
204
+  ARTICLE
205
+
206
+Responses
207
+
208
+First form (message-id specified)
209
+  220 0|n message-id    Article follows (multi-line)
210
+  430                   No article with that message-id
211
+
212
+Second form (article number specified)
213
+  220 n message-id      Article follows (multi-line)
214
+  412                   No newsgroup selected
215
+  423                   No article with that number
216
+
217
+Third form (current article number used)
218
+  220 n message-id      Article follows (multi-line)
219
+  412                   No newsgroup selected
220
+  420                   Current article number is invalid
221
+
222
+Parameters
223
+  number        Requested article number
224
+  n             Returned article number
225
+  message-id    Article message-id
226
+"
227
+  (article-command "article" client :article-number article-number
228
+                   :article-id article-id)
229
+  (when (string= "220" (client-status-code client))
230
+    (get-block-response client)))
231
+
232
+(defun head (&optional (client *default-client*)
233
+             &key article-number article-id)
234
+  "Retreives the article headers.
235
+ Syntax
236
+   HEAD message-id
237
+   HEAD number
238
+   HEAD
239
+
240
+ Responses
241
+
242
+ First form (message-id specified)
243
+   221 0|n message-id    Headers follow (multi-line)
244
+   430                   No article with that message-id
245
+
246
+ Second form (article number specified)
247
+   221 n message-id      Headers follow (multi-line)
248
+   412                   No newsgroup selected
249
+   423                   No article with that number
250
+
251
+ Third form (current article number used)
252
+   221 n message-id      Headers follow (multi-line)
253
+   412                   No newsgroup selected
254
+   420                   Current article number is invalid
255
+"
256
+  (article-command "head" client :article-number article-number
257
+                   :article-id article-id)
258
+  (when (string= "221" (client-status-code client))
259
+    (get-block-response client)))
260
+
261
+(defun body (&optional (client *default-client*)
262
+             &key article-number article-id)
263
+  "Retreives the article body
264
+Syntax
265
+  BODY message-id
266
+  BODY number
267
+  BODY
268
+
269
+Responses
270
+
271
+First form (message-id specified)
272
+  222 0|n message-id    Body follows (multi-line)
273
+  430                   No article with that message-id
274
+
275
+Second form (article number specified)
276
+  222 n message-id      Body follows (multi-line)
277
+  412                   No newsgroup selected
278
+  423                   No article with that number
279
+
280
+Third form (current article number used)
281
+  222 n message-id      Body follows (multi-line)
282
+  412                   No newsgroup selected
283
+  420                   Current article number is invalid
284
+
285
+Parameters
286
+  number        Requested article number
287
+  n             Returned article number
288
+  message-id    Article message-id
289
+"
290
+  (article-command "body" client :article-number article-number
291
+                   :article-id article-id)
292
+  (when (string= "222" (client-status-code client))
293
+    (get-block-response client)))
294
+
295
+(defun stat (&optional(client *default-client*)
296
+             &key article-number article-id)
297
+  "Determines if an article exists, or the message
298
+id of the article.
299
+
300
+Syntax
301
+  STAT message-id
302
+  STAT number
303
+  STAT
304
+
305
+Responses
306
+
307
+First form (message-id specified)
308
+  223 0|n message-id    Article exists
309
+  430                   No article with that message-id
310
+
311
+Second form (article number specified)
312
+  223 n message-id      Article exists
313
+  412                   No newsgroup selected
314
+  423                   No article with that number
315
+
316
+Third form (current article number used)
317
+  223 n message-id      Article exists
318
+  412                   No newsgroup selected
319
+  420                   Current article number is invalid
320
+
321
+Parameters
322
+  number        Requested article number
323
+  n             Returned article number
324
+  message-id    Article message-id
325
+"
326
+  (article-command "stat" client :article-number article-number
327
+                   :article-id article-id))
328
+
329
+(defun capabilities (&optional (client *default-client*))
330
+  "Lists the capabilities of the server.
331
+Syntax
332
+  CAPABILITIES [keyword]
333
+
334
+Responses
335
+  101    Capability list follows (multi-line)
336
+"
337
+  (block-command "CAPABILITIES" (list "101") client))
338
+
339
+(defun help (&optional (client *default-client*))
340
+  (block-command "help" (list "100") client))
341
+
342
+(defun block-command (command valid-codes
343
+                      &optional (client *default-client*))
344
+  "Sends a command that expects a block and retreives the block
345
+response."
346
+  (do-command command client)
347
+  (when (find (client-status-code client) valid-codes :test #'string=)
348
+    (get-block-response client)))
349
+
350
+(defun article-command (command &optional (client *default-client*)
351
+                        &key article-number article-id)
352
+  "Performs an articla related command. Can be one of 'ARTICLE', 'HEAD',
353
+'BODY', 'STAT'."
354
+  (if (and (null article-number)
355
+           (null article-id))
356
+      (do-command command client)
357
+      (let ((full-command
358
+             (string-trim " " (str command " "
359
+                                   (if (null article-number)
360
+                                       article-id
361
+                                       article-number)))))
362
+        (set-article client :article-number article-number
363
+                     :article-id article-id)
364
+        (do-command full-command client))))
365
+
366
+(defun set-article (&optional(client *default-client*)
367
+                    &key article-number article-id)
368
+  "Sets the article in the client"
369
+  (setf (client-article-number client) article-number
370
+        (client-article-id client) article-id))
371
+
372
+(defun get-status (&optional (client *default-client*))
373
+  "Reads the status line of the server response, retrurn status code and
374
+status message."
375
+  (let ((line (client-read-line client)))
376
+    (if line
377
+        (progn
378
+          (format t "<- ~A~%" line)
379
+          (force-output)
380
+          (let ((status-code (subseq line 0 (position #\space line)))
381
+                (status-message (subseq line (1+ (position #\space line)))))
382
+            (setf (client-status-message client) status-message
383
+                  (client-status-code client) status-code)
384
+            (values status-code status-message)))
385
+        (error "No response from the server"))))
386
+
387
+(defun authinfo (user pass &optional (client *default-client*))
388
+  "Authenticates the client."
389
+  (when (null client) (error "Can not authenticate NIL client"))
390
+  (when *cache-authinfo-p*
391
+    (setf (client-user client) user)
392
+    (setf (client-pass client) pass))
393
+  (authinfo-user user client)
394
+  (cond ((string= (client-status-code client) "381")
395
+         (authinfo-pass pass client)
396
+         (cond ((string= (client-status-code client) "281")
397
+                client)
398
+               (t (error "Couldnt authenticate client: ~a"
399
+                         (client-status-message client)))))
400
+        (t (error "Couldnt authenticate client: ~a"
401
+                  (client-status-message client)))))
402
+
403
+(defun register-client (client)
404
+  (format t "Registering client ~a ~%" (client-name client))
405
+  (when (null (find client *clients* :test #'eq))
406
+    (format t "Adding client to *clients*~%")
407
+    (setf *clients* (cons client *clients*))))
408
+
409
+(defun client-name (client)
410
+  (let ((name (str (client-host client) ":" (client-port client)))
411
+        (user (client-user client)))
412
+    (if user (str user "@" name) name)))
413
+
414
+(defun authinfo-user (user &optional (client *default-client*))
415
+  "Sends the 'authinfo user' command."
416
+  (let ((command (str "AUTHINFO USER " user)))
417
+    (client-write-line command client)
418
+    (get-status client)))
419
+
420
+(defun authinfo-pass (pass &optional (client *default-client*))
421
+  "Sends the 'authinfo pass' command."
422
+  (let ((command (str "AUTHINFO PASS " pass)))
423
+    (client-write-line command client)
424
+    (get-status client)))
425
+
426
+(defun date (&optional (client *default-client*))
427
+  (do-command "date" client))
428
+
429
+(defun last-article (&optional (client *default-client*))
430
+  (do-command "last" client))
431
+
432
+(defun next-article (&optional (client *default-client*))
433
+  (do-command "next" client))
434
+
435
+(defun get-block-response (&optional (client *default-client*))
436
+  "Reads a block response."
437
+  (let ((stream (client-stream client)))
438
+    (with-output-to-string (s)
439
+      (loop
440
+         (multiple-value-bind (line nl)
441
+             (client-read-line client nil stream)
442
+           ;; (format t "<- ~A~%" line)
443
+           (when (or (string= line ".
")
444
+                     (string= line ".")
445
+                     (eq line stream))
446
+             (return s))
447
+           (write-string line s)
448
+           (unless nl
449
+             (write-char #\Newline s)))))))
450
+
451
+(defun check-status-code (status-code valid-codes)
452
+  (if (find status-code valid-codes :test #'string=)
453
+      status-code
454
+      nil))
455
+
456
+(defun disconnect-client (&optional (client *default-client*))
457
+  (let ((stream (client-stream client)))
458
+    (when (or (null stream)
459
+              (not (open-stream-p stream)))
460
+      (format t "Client ~a:~a is not connected.~%"
461
+              (client-host client) (client-port client))
462
+      (return-from disconnect-client client))
463
+    (close stream)
464
+    (if (open-stream-p stream)
465
+        (error "Couldn't close stream: ~A~%" stream)
466
+        (setf (client-stream client) nil))
467
+    client))
468
+
469
+(defun destroy-client (&optional (client *default-client*))
470
+  (disconnect-client client)
471
+  (setf *clients* (delete client *clients*))
472
+  (when (eq *default-client* client)
473
+    (setf *default-client* (car *clients*))))
474
+
475
+(defun reconnect-client (&optional (client *default-client*))
476
+  (connect (client-host client) (client-port client) client)
477
+  (when (client-group client)
478
+    (let ((command (str "group " (client-group client))))
479
+      (client-write-line command client)
480
+      (get-status client)))
481
+  (cond ((client-article-number client)
482
+         (client-write-line
483
+          (str "stat " (client-article-number client))
484
+          (get-status client)))
485
+        ((client-article-id client)
486
+         (client-write-line (str "stat " (client-article-id client)) client)
487
+         (get-status client))
488
+        (t nil)))
489
+
490
+(defun read-authinfo-file (server-name &optional
491
+                           (file-name *authinfo-file-name*))
492
+  "Reads user name and passwords from authinfo file."
493
+  (when (not (probe-file file-name))
494
+    (return-from read-authinfo-file nil))
495
+  (let ((lines (split-sequence #\newline (slurp-file file-name))))
496
+    (loop for line in lines
497
+       do (let ((tokens (split-sequence #\space line)))
498
+            (when (and (string= server-name (cadr tokens))
499
+                       (>= (length tokens) 6))
500
+              (return-from read-authinfo-file
501
+                (list (fourth tokens) (sixth tokens))))))))
502
+
503
+(defun read-line-tls (tls-stream &optional (eof-error-p t) (eof-value nil)
504
+                      &key max-length)
505
+  "Reads a line from a TLS (SSL) stream."
506
+  (with-output-to-string (s)
507
+    (do ((b (read-byte tls-stream) (read-byte tls-stream))
508
+         (count 0 (1+ count)))
509
+        ((eq (code-char b) #\newline))
510
+      (when (eq b 'eof)
511
+        (if (and eof-error-p (= 0 (length s)))
512
+            (return-from read-line-tls (values eof-value t))
513
+            (return-from read-line-tls (values s t))))
514
+      (write-char (code-char b) s)
515
+      (when (and max-length (> count max-length))
516
+        (error "Line to long: ~a~%." s)))
517
+    s))
518
+
519
+(defun write-line-tls (line tls-stream )
520
+  "Writes a line to  a TLS (SSL) stream."
521
+  (let ((new-line (if (position #\newline line
522
+                                :start (1- (length line)))
523
+                      line
524
+                      (str line #\newline))))
525
+    (format t "-> ~a" new-line)
526
+    (write-sequence (babel:string-to-octets new-line) 
527
+                    tls-stream)))
528
+
529
+(defun client-read-line (client &optional (eof-error-p t) (eof-value nil))
530
+  (if (client-tls-p client)
531
+      (read-line-tls (client-stream client))
532
+      (read-line (client-stream client)
533
+                 eof-error-p eof-value)))
534
+
535
+(defun client-write-line (line client)
536
+  (if (client-tls-p client)
537
+      (write-line-tls line (client-stream client))
538
+      (progn
539
+        (format t "-> ~a~%" line)
540
+        (write-line (str line #\newline) (client-stream client))))
541
+  (force-output (client-stream client)))
542
+
543
+(defun ask-autinfo-from-user (server-name)
544
+  (format t "Please enter user name for server ~a~%"
545
+          server-name)
546
+  (let ((user-name (read-line)))
547
+    (format t "Please enter password for server ~a~%"
548
+            server-name)
549
+    (let ((passw (read-line)))
550
+      (list user-name passw))))
0 551
new file mode 100644
... ...
@@ -0,0 +1,32 @@
1
+
2
+(defpackage #:com.liotev.nntp.utils
3
+  (:use #:cl)
4
+  (:export #:str #:slurp-file))
5
+
6
+(defpackage #:com.liotev.nntp
7
+  (:nicknames #:cl-nntp #:nntp)
8
+  (:use #:cl #:split-sequence #:com.liotev.nntp.utils)
9
+  (:export #:connect
10
+           #:group
11
+           #:article
12
+           #:head
13
+           #:body
14
+           #:stat
15
+           #:last-article
16
+           #:next-article
17
+           #:authinfo
18
+           #:help
19
+           #:capabilities
20
+           #:date
21
+           #:quit
22
+           ;; #:listgroup
23
+           ;; #:mode-reader
24
+           ;; #:list
25
+           ;; #:newsgroups
26
+           ;; #:newnews
27
+           ;; #:post
28
+           ;; #:xhdr
29
+           ;; #:xover
30
+           ;; #:xpat
31
+           ))
32
+
0 33
new file mode 100644
... ...
@@ -0,0 +1,50 @@
1
+
2
+(in-package #:com.liotev.nntp.utils)
3
+
4
+(defun str (&rest parts)
5
+  (with-output-to-string (out)
6
+    (dolist (part parts)
7
+      (when (not (null part))
8
+        (write part :stream out :escape nil)))))
9
+
10
+(defun slurp-stream-as-seq (stream &key (element-type 'base-char))
11
+  (let ((seq (make-array (file-length stream) :element-type element-type :fill-pointer t)))
12
+    (setf (fill-pointer seq) (read-sequence seq stream))
13
+    seq))
14
+
15
+(defun slurp-stream-in-chunks (stream &key (element-type 'character) (chunk-length 1024))
16
+  (with-output-to-string (out)
17
+    (let ((seq (make-array chunk-length :element-type element-type
18
+                           :adjustable t
19
+                           :fill-pointer chunk-length)))
20
+      (loop
21
+         (setf (fill-pointer seq) (read-sequence seq stream))
22
+         (when (zerop (fill-pointer seq))
23
+           (return))
24
+         (write-sequence seq out)))))
25
+
26
+(defun slurp-stream (stream &key (element-type 'base-char))
27
+  (if (null (file-length stream))
28
+      (slurp-stream-in-chunks stream)
29
+      (slurp-stream-as-seq stream :element-type element-type)))
30
+
31
+(defun slurp-file (file-name &key (element-type 'base-char))
32
+  (with-open-file (s file-name :direction :input :element-type element-type)
33
+    (slurp-stream-as-seq s :element-type element-type)))
34
+
35
+(defun getenv (name &optional default)
36
+  #+CMU
37
+  (let ((x (assoc name ext:*environment-list*
38
+                  :test #'string=)))
39
+    (if x (cdr x) default))
40
+  #-CMU
41
+  (or
42
+   #+Allegro (sys:getenv name)
43
+   #+CLISP (ext:getenv name)
44
+   #+ECL (si:getenv name)
45
+   #+SBCL (sb-unix::posix-getenv name)
46
+   #+LISPWORKS (lispwrks:environment-variable name)
47
+   #+CCL (getenv name)
48
+   default))
49
+
50
+