;; quoted-printable.scm -- RFC2045 implementation
;; Copyright (c) 2005-2009 Alex Shinn.  All rights reserved.
;; BSD-style license: http://synthcode.com/license.txt

;; Procedure: quoted-printable-encode-string str [start-col max-col]
;;   Return a quoted-printable encoded representation of string
;;   according to the official standard as described in RFC2045.
;;
;;   ? and _ are always encoded for compatibility with RFC1522 encoding,
;;   and soft newlines are inserted as necessary to keep each lines
;;   length less than MAX-COL (default 76).  The starting column may be
;;   overridden with START-COL (default 0).

;; Procedure: quoted-printable-decode-string str [mime?]
;;   Return a quoted-printable decoded representation of string.  If
;;   MIME? is specified and true, _ will be decoded as as space in
;;   accordance with RFC1522.  No errors will be raised on invalid
;;   input.

;; Procedure: quoted-printable-encode [port start-col max-col]
;; Procedure: quoted-printable-decode [port start-col max-col]
;;   Variations of the above which read and write to ports.

;; Procedure: quoted-printable-encode-header enc str [start-col max-col]
;;   Return a quoted-printable encoded representation of string as
;;   above, wrapped in =?ENC?Q?...?= as per RFC1522, split across
;;   multiple MIME-header lines as needed to keep each lines length less
;;   than MAX-COL.  The string is encoded as is, and the encoding ENC is
;;   just used for the prefix, i.e. you are responsible for ensuring STR
;;   is already encoded according to ENC.

;; Example:

;; (define (mime-encode-header header value charset)
;;   (let ((prefix (string-append header ": "))
;;         (str (ces-convert value "UTF8" charset)))
;;     (string-append
;;      prefix
;;      (quoted-printable-encode-header charset str (string-length prefix)))))

;; This API is backwards compatible with the Gauche library
;; rfc.quoted-printable.

(define *default-max-col* 76)

;; Allow for RFC1522 quoting for headers by always escaping ? and _
(define (qp-encode str start-col max-col separator)
  (define (hex i) (integer->char (+ i (if (<= i 9) 48 55))))
  (let ((end (string-length str))
        (buf (make-string max-col)))
    (let lp ((i 0) (col start-col) (res '()))
      (cond
        ((= i end)
         (if (pair? res)
           (string-concatenate (reverse (cons (substring buf 0 col) res))
                               separator)
           (substring buf start-col col)))
        ((>= col (- max-col 3))
         (lp i 0 (cons (substring buf (if (pair? res) 0 start-col) col) res)))
        (else
         (let ((c (char->integer (string-ref str i))))
           (cond
             ((and (<= 33 c 126) (not (memq c '(61 63 95))))
              (string-set! buf col (integer->char c))
              (lp (+ i 1) (+ col 1) res))
             (else
              (string-set! buf col #\=)
              (string-set! buf (+ col 1) (hex (arithmetic-shift c -4)))
              (string-set! buf (+ col 2) (hex (bitwise-and c #b1111)))
              (lp (+ i 1) (+ col 3) res)))))))))

(define (quoted-printable-encode-string . o)
  (let ((src (if (pair? o) (car o) (current-input-port)))
        (start-col (if (and (pair? o) (pair? (cdr o))) (cadr o) 0))
        (max-col (if (and (pair? o) (pair? (cdr o)) (pair? (cddr o)))
                     (caddr o)
                     *default-max-col*)))
    (qp-encode (if (string? src) src (read-string #f src))
               start-col max-col "=\r\n")))

(define (quoted-printable-encode . o)
  (display (apply (quoted-printable-encode-string o))))

(define (quoted-printable-encode-header encoding . o)
  (let ((src (if (pair? o) (car o) (current-input-port)))
        (start-col (if (and (pair? o) (pair? (cdr o))) (cadr o) 0))
        (max-col (if (and (pair? o) (pair? (cdr o)) (pair? (cddr o)))
                     (caddr o)
                     *default-max-col*))
        (nl (if (and (pair? o) (pair? (cdr o)) (pair? (cddr o)) (pair? (cdddr o)))
                (cadddr o)
                "\r\n")))
    (let* ((prefix (string-append "=?" encoding "?Q?"))
           (prefix-length (+ 2 (string-length prefix)))
           (separator (string-append "?=" nl "\t" prefix))
           (effective-max-col (- max-col prefix-length)))
      (string-append prefix
                     (qp-encode (if (string? src) src (read-string #f src))
                                start-col effective-max-col separator)
                     "?="))))

(define (quoted-printable-decode-string  . o)
  (define (hex? c) (or (char-numeric? c) (<= 65 (char->integer c) 70)))
  (define (unhex1 c)
    (let ((i (char->integer c))) (if (>= i 65) (- i 55) (- i 48))))
  (define (unhex c1 c2)
    (integer->char (+ (arithmetic-shift (unhex1 c1) 4) (unhex1 c2))))
  (let ((src (if (pair? o) (car o) (current-input-port)))
        (mime-header? (and (pair? o) (pair? (cdr o)) (car (cdr o)))))
    (let* ((str (if (string? src) src (read-string #f src)))
           (end (string-length str)))
      (call-with-output-string
        (lambda (out)
          (let lp ((i 0))
            (cond
             ((< i end)
              (let ((c (string-ref str i)))
                (case c
                  ((#\=)                ; = escapes
                   (cond
                    ((< (+ i 2) end)
                     (let ((c2 (string-ref str (+ i 1))))
                       (cond
                        ((eq? c2 #\newline) (lp (+ i 2)))
                        ((eq? c2 #\return)
                         (lp (if (eq? (string-ref str (+ i 2)) #\newline)
                                 (+ i 3)
                                 (+ i 2))))
                        ((hex? c2)
                         (let ((c3 (string-ref str (+ i 2))))
                           (if (hex? c3) (write-char (unhex c2 c3) out))
                           (lp (+ i 3))))
                        (else (lp (+ i 3))))))))
                  ((#\_)                ; maybe translate _ to space
                   (write-char (if mime-header? #\space c) out)
                   (lp (+ i 1)))
                  ((#\space #\tab)      ; strip trailing whitespace
                   (let lp2 ((j (+ i 1)))
                     (cond
                      ((not (= j end))
                       (case (string-ref str j)
                         ((#\space #\tab) (lp2 (+ j 1)))
                         ((#\newline)
                          (lp (+ j 1)))
                         ((#\return)
                          (let ((k (+ j 1)))
                            (lp (if (and (< k end)
                                         (eqv? #\newline (string-ref str k)))
                                    (+ k 1) k))))
                         (else (display (substring str i j) out) (lp j)))))))
                  (else                 ; a literal char
                   (write-char c out)
                   (lp (+ i 1)))))))))))))

(define (quoted-printable-decode . o)
  (display (apply quoted-printable-decode-string o)))