├── .gitignore
├── test
├── image3.png
├── test_private.pem
├── mailcomposer.js
└── textfile.txt
├── .travis.yml
├── lib
├── topunycode.js
├── urlfetch.js
└── mailcomposer.js
├── LICENSE
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
--------------------------------------------------------------------------------
/test/image3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/drFabio/mailcomposer/master/test/image3.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 0.8
4 | - "0.10"
5 | - 0.11
6 |
7 | notifications:
8 | email:
9 | recipients:
10 | - andris@kreata.ee
11 | on_success: change
12 | on_failure: change
13 |
--------------------------------------------------------------------------------
/lib/topunycode.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var punycode = require("punycode");
4 |
5 | module.exports = function(address){
6 | return address.replace(/((?:https?:\/\/)?.*\@)?([^\/]*)/, function(o, start, domain){
7 | var domainParts = domain.split(/\./).map(punycode.toASCII.bind(punycode));
8 | return (start || "") + domainParts.join(".");
9 | });
10 | };
--------------------------------------------------------------------------------
/test/test_private.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIBywIBAAJhANCx7ncKUfQ8wBUYmMqq6ky8rBB0NL8knBf3+uA7q/CSxpX6sQ8N
3 | dFNtEeEd7gu7BWEM7+PkO1P0M78eZOvVmput8BP9R44ARpgHY4V0qSCdUt4rD32n
4 | wfjlGbh8p5ua5wIDAQABAmAm+uUQpQPTu7kg95wqVqw2sxLsa9giT6M8MtxQH7Uo
5 | 1TF0eAO0TQ4KOxgY1S9OT5sGPVKnag258m3qX7o5imawcuyStb68DQgAUg6xv7Af
6 | AqAEDfYN5HW6xK+X81jfOUECMQDr7XAS4PERATvgb1B3vRu5UEbuXcenHDYgdoyT
7 | 3qJFViTbep4qeaflF0uF9eFveMcCMQDic10rJ8fopGD7/a45O4VJb0+lRXVdqZxJ
8 | QzAp+zVKWqDqPfX7L93SQLzOGhdd7OECMQDeQyD7WBkjSQNMy/GF7I1qxrscIxNN
9 | VqGTcbu8Lti285Hjhx/sqhHHHGwU9vB7oM8CMQDKTS3Kw/s/xrot5O+kiZwFgr+w
10 | cmDrj/7jJHb+ykFNb7GaEkiSYqzUjKkfpweBDYECMFJUyzuuFJAjq3BXmGJlyykQ
11 | TweUw+zMVdSXjO+FCPcYNi6CP1t1KoESzGKBVoqA/g==
12 | -----END RSA PRIVATE KEY-----
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Andris Reinman
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
16 | SOFTWARE.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mailcomposer",
3 | "description": "Compose E-Mail messages",
4 | "version": "0.2.8",
5 | "author": "Andris Reinman",
6 | "maintainers": [
7 | {
8 | "name": "andris",
9 | "email": "andris@node.ee"
10 | }
11 | ],
12 | "repository": {
13 | "type": "git",
14 | "url": "http://github.com/andris9/mailcomposer.git"
15 | },
16 | "scripts": {
17 | "test": "nodeunit test/"
18 | },
19 | "main": "./lib/mailcomposer",
20 | "licenses": [
21 | {
22 | "type": "MIT",
23 | "url": "http://github.com/andris9/mailcomposer/blob/master/LICENSE"
24 | }
25 | ],
26 | "dependencies": {
27 | "mimelib": "~0.2.14",
28 | "mime": "1.2.9",
29 | "he": "~0.3.6",
30 | "punycode": "~1.2.3",
31 | "follow-redirects": "0.0.3",
32 | "dkim-signer": "~0.1.0"
33 | },
34 | "devDependencies": {
35 | "nodeunit": "*",
36 | "mailparser": "~0.4.0"
37 | },
38 | "engine": {
39 | "node": ">=0.4"
40 | },
41 | "keywords": [
42 | "e-mail",
43 | "mime",
44 | "parser"
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/lib/urlfetch.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var http = require('follow-redirects').http,
4 | https = require('follow-redirects').https,
5 | urllib = require("url"),
6 | Stream = require('stream').Stream;
7 |
8 | /**
9 | * @namespace URLFetch
10 | * @name urlfetch
11 | */
12 | module.exports = openUrlStream;
13 |
14 | /**
15 | *
Open a stream to a specified URL
16 | *
17 | * @memberOf urlfetch
18 | * @param {String} url URL to open
19 | * @param {Object} [options] Optional options object
20 | * @param {String} [options.userAgent="mailcomposer"] User Agent for the request
21 | * @return {Stream} Stream for the URL contents
22 | */
23 | function openUrlStream(url, options){
24 | options = options || {};
25 | var urlparts = urllib.parse(url),
26 | urloptions = {
27 | host: urlparts.hostname,
28 | port: urlparts.port || (urlparts.protocol=="https:"?443:80),
29 | path: urlparts.path || urlparts.pathname,
30 | method: "GET",
31 | headers: {
32 | "User-Agent": options.userAgent || "mailcomposer"
33 | },
34 | agent: false
35 | },
36 | client = (urlparts.protocol=="https:"?https:http),
37 | stream = new Stream(),
38 | request;
39 |
40 | stream.resume = function(){};
41 |
42 | if(urlparts.auth){
43 | urloptions.auth = urlparts.auth;
44 | }
45 |
46 | request = client.request(urloptions, function(response) {
47 | if((response.statusCode || 0).toString().charAt(0) != "2"){
48 | stream.emit("error", "Invalid status code " + (response.statusCode || 0));
49 | return;
50 | }
51 |
52 | response.on('error', function(err) {
53 | stream.emit("error", err);
54 | });
55 |
56 | response.on('data', function(chunk) {
57 | stream.emit("data", chunk);
58 | });
59 |
60 | response.on('end', function(chunk) {
61 | if(chunk){
62 | stream.emit("data", chunk);
63 | }
64 | stream.emit("end");
65 | });
66 | });
67 | request.end();
68 |
69 | request.on('error', function(err) {
70 | stream.emit("error", err);
71 | });
72 |
73 | return stream;
74 | }
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mailcomposer
2 |
3 | **mailcomposer** is a Node.JS module for generating e-mail messages that can be
4 | streamed to SMTP or file.
5 |
6 | This is a standalone module that only generates raw e-mail source, you need to
7 | write your own or use an existing transport mechanism (SMTP client, Amazon SES,
8 | SendGrid etc). **mailcomposer** frees you from the tedious task of generating
9 | [rfc2822](http://tools.ietf.org/html/rfc2822) compatible messages.
10 |
11 | [](http://travis-ci.org/andris9/mailcomposer)
12 |
13 | **mailcomposer** supports:
14 |
15 | * **Unicode** to use any characters ✔
16 | * **HTML** content as well as **plain text** alternative
17 | * **Attachments** and streaming for larger files (use strings, buffers, files or binary streams as attachments)
18 | * **Embedded images** in HTML
19 | * **DKIM** signing
20 | * usage of **your own** transport mechanism
21 |
22 | ## Support mailcomposer development
23 |
24 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=DB26KWR2BQX5W)
25 |
26 | ## Installation
27 |
28 | Install through NPM
29 |
30 | npm install mailcomposer
31 |
32 | ## Usage
33 |
34 | ### Include mailcomposer module
35 |
36 | ```javascript
37 | var MailComposer = require("mailcomposer").MailComposer;
38 | ```
39 |
40 | ### Create a new `MailComposer` instance
41 |
42 | ```javascript
43 | var mailcomposer = new MailComposer([options]);
44 | ```
45 |
46 | Where `options` is an optional options object with the following possible properties:
47 |
48 | * **escapeSMTP** - if set replaces dots in the beginning of a line with double dots
49 | * **encoding** - sets transfer encoding for the textual parts (defaults to `"quoted-printable"`)
50 | * **charset** - sets output character set for strings (defaults to `"utf-8"`)
51 | * **keepBcc** - if set to true, includes `Bcc:` field in the message headers. Useful for *sendmail* command.
52 | * **forceEmbeddedImages** - convert image urls and absolute paths in HTML to embedded attachments.
53 |
54 | ### Simple example
55 |
56 | The following example generates a simple e-mail message with plaintext and html
57 | body.
58 |
59 | ```javascript
60 | var MailComposer = require("mailcomposer").MailComposer;
61 | mailcomposer = new MailComposer(),
62 | fs = require("fs");
63 |
64 | // add additional header field
65 | mailcomposer.addHeader("x-mailer", "Nodemailer 1.0");
66 |
67 | // setup message data
68 | mailcomposer.setMessageOption({
69 | from: "andris@tr.ee",
70 | to: "andris@node.ee",
71 | body: "Hello world!",
72 | html: "Hello world!"
73 | });
74 |
75 | mailcomposer.streamMessage();
76 |
77 | // pipe the output to a file
78 | mailcomposer.pipe(fs.createWriteStream("test.eml"));
79 | ```
80 |
81 | The output for such a script (the contents for "test.eml") would look like:
82 |
83 | MIME-Version: 1.0
84 | X-Mailer: Nodemailer 1.0
85 | From: andris@tr.ee
86 | To: andris@node.ee
87 | Content-Type: multipart/alternative;
88 | boundary="----mailcomposer-?=_1-1328088797399"
89 |
90 | ------mailcomposer-?=_1-1328088797399
91 | Content-Type: text/plain; charset=utf-8
92 | Content-Transfer-Encoding: quoted-printable
93 |
94 | Hello world!
95 | ------mailcomposer-?=_1-1328088797399
96 | Content-Type: text/html; charset=utf-8
97 | Content-Transfer-Encoding: quoted-printable
98 |
99 | Hello world!
100 | ------mailcomposer-?=_1-1328088797399--
101 |
102 | ## API
103 |
104 | ### Add custom headers
105 |
106 | Headers can be added with `mailcomposer.addHeader(key, value[, formatted])` where `formatted` indicates if the value should be kept as is. If the value is missing or falsy, header value is sanitized and folded. If true, the value is passed to output as is.
107 |
108 | ```javascript
109 | var mailcomposer = new MailComposer();
110 | mailcomposer.addHeader("x-mailer", "Nodemailer 1.0");
111 | ```
112 |
113 | If you add an header value with the same key several times, all of the values will be used
114 | in the generated header. For example:
115 |
116 | ```javascript
117 | mailcomposer.addHeader("x-mailer", "Nodemailer 1.0");
118 | mailcomposer.addHeader("x-mailer", "Nodemailer 2.0");
119 | ```
120 |
121 | Will be generated into
122 |
123 | ...
124 | X-Mailer: Nodemailer 1.0
125 | X-Mailer: Nodemailer 2.0
126 | ...
127 |
128 | The contents of the field value is not edited in any way (except for the folding),
129 | so if you want to use unicode symbols you need to escape these to mime words
130 | by yourself. Exception being object values - in this case the object
131 | is automatically JSONized and mime encoded.
132 |
133 | ```javascript
134 | // using objects as header values is allowed (will be converted to JSON)
135 | var apiOptions = {};
136 | apiOptions.category = "newuser";
137 | apiOptions.tags = ["user", "web"];
138 | mailcomposer.addHeader("X-SMTPAPI", apiOptions)
139 | ```
140 |
141 | ### Add message parts
142 |
143 | You can set message sender, receiver, subject line, message body etc. with
144 | `mailcomposer.setMessageOption(options)` where options is an object with the
145 | data to be set. This function overwrites any previously set values with the
146 | same key
147 |
148 | The following example creates a simple e-mail with sender being `andris@tr.ee`,
149 | receiver `andris@node.ee` and plaintext part of the message as `Hello world!`:
150 |
151 | ```javascript
152 | mailcomposer.setMessageOption({
153 | from: "andris@tr.ee",
154 | to: "andris@node.ee",
155 | body: "Hello world!"
156 | });
157 | ```
158 |
159 | Possible options that can be used are (all fields accept unicode):
160 |
161 | * **from** (alias `sender`) - the sender of the message. If several addresses are given, only the first one will be used
162 | * **to** - receivers for the `To:` field
163 | * **cc** - receivers for the `Cc:` field
164 | * **bcc** - receivers for the `Bcc:` field
165 | * **replyTo** (alias `reply_to`) - e-mail address for the `Reply-To:` field
166 | * **inReplyTo** - The message-id this message is replying
167 | * **references** - Message-id list
168 | * **subject** - the subject line of the message
169 | * **body** (alias `text`) - the plaintext part of the message
170 | * **html** - the HTML part of the message
171 | * **envelope** - optional SMTP envelope, if auto generated envelope is not suitable
172 |
173 | This method can be called several times
174 |
175 | ```javascript
176 | mailcomposer.setMessageOption({from: "andris@tr.ee"});
177 | mailcomposer.setMessageOption({to: "andris@node.ee"});
178 | mailcomposer.setMessageOption({body: "Hello world!"});
179 | ```
180 |
181 | Trying to set the same key several times will yield in overwrite
182 |
183 | ```javascript
184 | mailcomposer.setMessageOption({body: "Hello world!"});
185 | mailcomposer.setMessageOption({body: "Hello world?"});
186 | // body contents will be "Hello world?"
187 | ```
188 |
189 | ### Address format
190 |
191 | All e-mail address fields take structured e-mail lists (comma separated)
192 | as the input. Unicode is allowed for all the parts (receiver name, e-mail username
193 | and domain) of the address. If the domain part contains unicode symbols, it is
194 | automatically converted into punycode, user part will be converted into UTF-8
195 | mime word.
196 |
197 | E-mail addresses can be a plain e-mail addresses
198 |
199 | username@example.com
200 |
201 | or with a formatted name
202 |
203 | 'Ноде Майлер'
204 |
205 | Or in case of comma separated lists, the formatting can be mixed
206 |
207 | username@example.com, 'Ноде Майлер' , "Name, User"
208 |
209 | ### SMTP envelope
210 |
211 | SMTP envelope is usually auto generated from `from`, `to`, `cc` and `bcc` fields but
212 | if for some reason you want to specify it yourself, you can do it with `envelope` property.
213 |
214 | `envelope` is an object with the following params: `from`, `to`, `cc` and `bcc` just like
215 | with regular mail options. You can also use the regular address format.
216 |
217 | ```javascript
218 | mailOptions = {
219 | ...,
220 | from: "mailer@node.ee",
221 | to: "daemon@node.ee",
222 | envelope: {
223 | from: "Daemon ",
224 | to: "mailer@node.ee, Mailer "
225 | }
226 | }
227 | ```
228 |
229 | ### Add attachments
230 |
231 | Attachments can be added with `mailcomposer.addAttachment(attachment)` where
232 | `attachment` is an object with attachment (meta)data with the following possible
233 | properties:
234 |
235 | * **fileName** (alias `filename`) - filename to be reported as the name of the attached file, use of unicode is allowed
236 | * **cid** - content id for using inline images in HTML message source
237 | * **contents** - String or a Buffer contents for the attachment
238 | * **filePath** - path to a file or an URL if you want to stream the file instead of including it (better for larger attachments)
239 | * **streamSource** - Stream object for arbitrary binary streams if you want to stream the contents (needs to support *pause*/*resume*)
240 | * **contentType** - content type for the attachment, if not set will be derived from the `fileName` property
241 | * **contentDisposition** - content disposition type for the attachment, defaults to "attachment"
242 | * **userAgent** - User-Agent string to be used if the fileName points to an URL
243 |
244 | One of `contents`, `filePath` or `streamSource` must be specified, if none is
245 | present, the attachment will be discarded. Other fields are optional.
246 |
247 | Attachments can be added as many as you want.
248 |
249 | **Using embedded images in HTML**
250 |
251 | Attachments can be used as embedded images in the HTML body. To use this
252 | feature, you need to set additional property of the attachment - `cid`
253 | (unique identifier of the file) which is a reference to the attachment file.
254 | The same `cid` value must be used as the image URL in HTML (using `cid:` as
255 | the URL protocol, see example below).
256 |
257 | NB! the cid value should be as unique as possible!
258 |
259 | ```javascript
260 | var cid_value = Date.now() + '.image.jpg';
261 |
262 | var html = 'Embedded image:
';
263 |
264 | var attachment = {
265 | fileName: "image.png",
266 | filePath: "/static/images/image.png",
267 | cid: cid_value
268 | };
269 | ```
270 |
271 | **Automatic embedding images**
272 |
273 | If you want to convert images in the HTML to embedded images automatically, you can
274 | set mailcomposer option `forceEmbeddedImages` to true. In this case all images in
275 | the HTML that are either using an absolute URL (http://...) or absolute file path
276 | (/path/to/file) are replaced with embedded attachments.
277 |
278 | For example when using this code
279 |
280 | ```javascript
281 | var mailcomposer = new MailComposer({forceEmbeddedImages: true});
282 | mailcomposer.setMessageOption({
283 | html: 'Embedded image:
'
284 | });
285 | ```
286 |
287 | The image linked is fetched and added automatically as an attachment and the url
288 | in the HTML is replaced automatically with a proper `cid:` string.
289 |
290 | ### Add alternatives to HTML and text
291 |
292 | In addition to text and HTML, any kind of data can be inserted as an alternative content of the main body - for example a word processing document with the same text as in the HTML field. It is the job of the e-mail client to select and show the best fitting alternative to the reader.
293 |
294 | Alternatives to text and HTML can be added with `mailcomposer.addAlternative(alternative)` where
295 | `alternative` is an object with alternative (meta)data with the following possible
296 | properties:
297 |
298 | * **contents** - String or a Buffer contents for the attachment
299 | * **contentType** - optional content type for the attachment, if not set will be set to "application/octet-stream"
300 | * **contentEncoding** - optional value of how the data is encoded, defaults to "base64"
301 |
302 | If `contents` is empty, the alternative will be discarded. Other fields are optional.
303 |
304 | **Usage example:**
305 |
306 | ```javascript
307 | // add HTML "alternative"
308 | mailcomposer.setMessageOption({
309 | html: "Hello world!"
310 | });
311 |
312 | // add Markdown alternative
313 | mailcomposer.addAlternative({
314 | contentType: "text/x-web-markdown",
315 | contents: "**Hello world!**"
316 | });
317 | ```
318 |
319 | If the receiving e-mail client can render messages in Markdown syntax as well, it could prefer
320 | to display this alternative as the main content of the message.
321 |
322 | Alternatives can be added as many as you want.
323 |
324 | ### DKIM Signing
325 |
326 | **mailcomposer** supports DKIM signing with very simple setup. Use this with caution
327 | though since the generated message needs to be buffered entirely before it can be
328 | signed - in this case the streaming capability offered by mailcomposer is illusionary,
329 | there will only be one `'data'` event with the entire message. Not a big deal with
330 | small messages but might consume a lot of RAM when using larger attachments.
331 |
332 | Set up the DKIM signing with `useDKIM` method:
333 |
334 | ```javascript
335 | mailcomposer.useDKIM(dkimOptions)
336 | ```
337 |
338 | Where `dkimOptions` includes necessary options for signing
339 |
340 | * **domainName** - the domainname that is being used for signing
341 | * **keySelector** - key selector. If you have set up a TXT record with DKIM public key at *zzz._domainkey.example.com* then `zzz` is the selector
342 | * **privateKey** - DKIM private key that is used for signing as a string
343 | * **headerFieldNames** - optional colon separated list of header fields to sign, by default all fields suggested by RFC4871 #5.5 are used
344 |
345 | **NB!** Currently if several header fields with the same name exists, only the last one (the one in the bottom) is signed.
346 |
347 | Example:
348 |
349 | ```javascript
350 | mailcomposer.setMessageOption({from: "andris@tr.ee"});
351 | mailcomposer.setMessageOption({to: "andris@node.ee"});
352 | mailcomposer.setMessageOption({body: "Hello world!"});
353 | mailcomposer.useDKIM({
354 | domainName: "node.ee",
355 | keySelector: "dkim",
356 | privateKey: fs.readFileSync("private_key.pem")
357 | });
358 | ```
359 |
360 | ### Start streaming
361 |
362 | When the message data is setup, streaming can be started. After this it is not
363 | possible to add headers, attachments or change body contents.
364 |
365 | ```javascript
366 | mailcomposer.streamMessage();
367 | ```
368 |
369 | This generates `'data'` events for the message headers and body and final `'end'` event.
370 | As `MailComposer` objects are Stream instances, these can be piped
371 |
372 | ```javascript
373 | // save the output to a file
374 | mailcomposer.streamMessage();
375 | mailcomposer.pipe(fs.createWriteStream("out.txt"));
376 | ```
377 |
378 | ### Compile the message in one go
379 |
380 | If you do not want to use the streaming possibilities, you can compile the entire
381 | message into a string in one go with `buildMessage`.
382 |
383 | ```javascript
384 | mailcomposer.buildMessage(function(err, messageSource){
385 | console.log(err || messageSource);
386 | });
387 | ```
388 |
389 | The function is actually just a wrapper around `streamMessage` and emitted events.
390 |
391 | ## Envelope
392 |
393 | Envelope can be generated with an `getEnvelope()` which returns an object
394 | that includes a `from` address (string) and a list of `to` addresses (array of
395 | strings) suitable for forwarding to a SMTP server as `MAIL FROM:` and `RCPT TO:`.
396 |
397 | ```javascript
398 | console.log(mailcomposer.getEnvelope());
399 | // {from:"sender@example.com", to:["receiver@example.com"]}
400 | ```
401 |
402 | **NB!** both `from` and `to` properties might be missing from the envelope object
403 | if corresponding addresses were not detected from the e-mail.
404 |
405 | ## Running tests
406 |
407 | Tests are run with [nodeunit](https://github.com/caolan/nodeunit)
408 |
409 | Run
410 |
411 | npm test
412 |
413 | ## License
414 |
415 | **MIT**
416 |
--------------------------------------------------------------------------------
/test/mailcomposer.js:
--------------------------------------------------------------------------------
1 | var testCase = require('nodeunit').testCase,
2 | MailComposer = require("../lib/mailcomposer").MailComposer,
3 | toPunycode = require("../lib/topunycode"),
4 | MailParser = require("mailparser").MailParser,
5 | fs = require("fs"),
6 | mime = require("mime"),
7 | http = require("http");
8 |
9 |
10 | var HTTP_PORT = 9437;
11 |
12 | exports["General tests"] = {
13 |
14 | "Create a new MailComposer object": function(test){
15 | var mailcomposer = new MailComposer();
16 | test.equal(typeof mailcomposer.on, "function");
17 | test.equal(typeof mailcomposer.emit, "function");
18 | test.done();
19 | },
20 |
21 | "Normalize key names": function(test){
22 | var normalizer = MailComposer.prototype._normalizeKey;
23 |
24 | test.equal(normalizer("abc"), "Abc");
25 | test.equal(normalizer("aBC"), "Abc");
26 | test.equal(normalizer("ABC"), "Abc");
27 | test.equal(normalizer("a-b-c"), "A-B-C");
28 | test.equal(normalizer("ab-bc"), "Ab-Bc");
29 | test.equal(normalizer("ab-bc-cd"), "Ab-Bc-Cd");
30 | test.equal(normalizer("AB-BC-CD"), "Ab-Bc-Cd");
31 | test.equal(normalizer("mime-version"), "MIME-Version"); // special case
32 |
33 | test.done();
34 | },
35 |
36 | "Add header": function(test){
37 | var mc = new MailComposer();
38 | test.equal(typeof mc._headers["Test-Key"], "undefined");
39 | mc.addHeader("test-key", "first");
40 | test.equal(mc._headers["Test-Key"], "first");
41 | mc.addHeader("test-key", "second");
42 | test.deepEqual(mc._headers["Test-Key"], ["first","second"]);
43 | mc.addHeader("test-key", "third");
44 | test.deepEqual(mc._headers["Test-Key"], ["first","second","third"]);
45 | test.done();
46 | },
47 |
48 | "Add formatted header": function(test){
49 | var mc = new MailComposer();
50 |
51 | mc.addHeader("test-key", "first", true);
52 | test.deepEqual(mc._headers["Test-Key"], {value: "first", formatted: true});
53 |
54 | mc.addHeader("test-key", "second", true);
55 | test.deepEqual(mc._headers["Test-Key"], [{value: "first", formatted: true}, {value: "second", formatted: true}]);
56 |
57 | mc.addHeader("test-key", "third");
58 | test.deepEqual(mc._headers["Test-Key"], [{value: "first", formatted: true}, {value: "second", formatted: true},"third"]);
59 | test.done();
60 | },
61 |
62 | "Get header": function(test){
63 | var mc = new MailComposer();
64 | test.equal(mc._getHeader("test-key"), "");
65 | mc.addHeader("test-key", "first");
66 | test.equal(mc._getHeader("test-key"), "first");
67 | mc.addHeader("test-key", "second");
68 | test.deepEqual(mc._getHeader("test-key"), ["first", "second"]);
69 | test.done();
70 | },
71 |
72 | "Get formatted header": function(test){
73 | var mc = new MailComposer();
74 |
75 | mc.addHeader("test-key", "first", true);
76 | test.equal(mc._getHeader("test-key"), "first");
77 | mc.addHeader("test-key", "second");
78 | test.deepEqual(mc._getHeader("test-key"), ["first", "second"]);
79 | test.done();
80 | },
81 |
82 | "Uppercase header keys": function(test){
83 | var mc = new MailComposer();
84 |
85 | mc.addHeader("X-TEST", "first");
86 | test.equal(mc._headers["X-TEST"], "first");
87 |
88 | mc.addHeader("TEST", "second");
89 | test.equal(mc._headers["Test"], "second");
90 |
91 | test.done();
92 | },
93 |
94 | "Folded header values": function(test){
95 | var mc = new MailComposer(),
96 | strings = [
97 | ['This is a\r\n folded header with extra space','This is a folded header with extra space'],
98 | ['This is a header\n with just a LF','This is a headerwith just a LF'],
99 | ['This is a header\r with just a CR','This is a headerwith just a CR'],
100 | ['This is a plain header','This is a plain header'],
101 | ['This is a\r\n split header with no extra WS','This is asplit header with no extra WS']
102 | ];
103 |
104 | strings.forEach(function(val){
105 | test.equal(mc._sanitizeHeaderValue(val[0]), val[1]);
106 | });
107 |
108 | test.done();
109 | },
110 |
111 | "Set object header": function(test){
112 | var mc = new MailComposer();
113 |
114 | var testObj = {
115 | stringValue: "String with unicode symbols: ÕÄÖÜŽŠ",
116 | arrayValue: ["hello ÕÄÖÜ", 12345],
117 | objectValue: {
118 | customerId: "12345"
119 | }
120 | };
121 |
122 | mc.addHeader("x-mytest-string", "first");
123 | mc.addHeader("x-mytest-json", testObj);
124 |
125 | mc.streamMessage();
126 |
127 | //mc.on("data", function(c){console.log(c.toString("utf-8"))})
128 |
129 | var mp = new MailParser();
130 |
131 | mc.pipe(mp);
132 |
133 | mp.on("end", function(mail){
134 | test.equal(mail.headers['x-mytest-string'], "first");
135 | test.deepEqual(JSON.parse(mail.headers['x-mytest-json']), testObj);
136 | //console.log(mail)
137 | test.done();
138 | });
139 | },
140 |
141 | "Add message option": function(test){
142 | var mc = new MailComposer();
143 | test.equal(typeof mc._message.subject, "undefined");
144 |
145 | mc.setMessageOption({
146 | subject: "Test1",
147 | body: "Test2",
148 | nonexistent: "Test3"
149 | });
150 |
151 | test.equal(mc._message.subject, "Test1");
152 | test.equal(mc._message.body, "Test2");
153 | test.equal(typeof mc._message.nonexistent, "undefined");
154 |
155 | mc.setMessageOption({
156 | subject: "Test4"
157 | });
158 |
159 | test.equal(mc._message.subject, "Test4");
160 | test.equal(mc._message.body, "Test2");
161 |
162 | test.done();
163 | },
164 |
165 | "Detect mime type": function(test){
166 | var mc = new MailComposer();
167 |
168 | test.equal(mime.lookup("test.txt"), "text/plain");
169 | test.equal(mime.lookup("test.unknown"), "application/octet-stream");
170 |
171 | test.done();
172 | },
173 |
174 | "keepBcc off": function(test){
175 | var mc = new MailComposer();
176 | mc.setMessageOption({bcc: "andris@node.ee"});
177 | mc._buildMessageHeaders();
178 | test.ok(!mc._getHeader("Bcc"));
179 | test.done();
180 | },
181 |
182 | "keepBcc on": function(test){
183 | var mc = new MailComposer({keepBcc: true});
184 | mc.setMessageOption({bcc: "andris@node.ee"});
185 | mc._buildMessageHeaders();
186 | test.equal(mc._getHeader("Bcc"), "andris@node.ee");
187 | test.done();
188 | },
189 |
190 | "zero length cc": function(test){
191 | var mc = new MailComposer({keepBcc: true});
192 | mc.setMessageOption({cc: ""});
193 | mc._buildMessageHeaders();
194 | test.equal(mc._getHeader("cc"), "");
195 | test.done();
196 | },
197 |
198 | "Auto references from in-reply-to 1": function(test){
199 | var mc = new MailComposer();
200 | mc.setMessageOption({
201 | inReplyTo: "test"
202 | });
203 | mc._buildMessageHeaders();
204 |
205 | test.equal(mc._getHeader("in-reply-to"), "");
206 | test.equal(mc._getHeader("references"), "");
207 | test.done();
208 | },
209 |
210 | "Auto references from in-reply-to 2": function(test){
211 | var mc = new MailComposer();
212 | mc.setMessageOption({
213 | inReplyTo: "test",
214 | references: "test"
215 | });
216 | mc._buildMessageHeaders();
217 |
218 | test.equal(mc._getHeader("in-reply-to"), "");
219 | test.equal(mc._getHeader("references"), "");
220 | test.done();
221 | },
222 |
223 | "Auto references from in-reply-to 3": function(test){
224 | var mc = new MailComposer();
225 | mc.setMessageOption({
226 | inReplyTo: "test",
227 | references: ["test"]
228 | });
229 | mc._buildMessageHeaders();
230 |
231 | test.equal(mc._getHeader("in-reply-to"), "");
232 | test.equal(mc._getHeader("references"), "");
233 | test.done();
234 | },
235 |
236 | "Auto references from in-reply-to 4": function(test){
237 | var mc = new MailComposer();
238 | mc.setMessageOption({
239 | inReplyTo: "test",
240 | references: [""]
241 | });
242 | mc._buildMessageHeaders();
243 |
244 | test.equal(mc._getHeader("in-reply-to"), "");
245 | test.equal(mc._getHeader("references"), " ");
246 | test.done();
247 | },
248 |
249 | "Auto references from in-reply-to 5": function(test){
250 | var mc = new MailComposer();
251 | mc.setMessageOption({
252 | inReplyTo: "test",
253 | references: ["test2 test"]
254 | });
255 | mc._buildMessageHeaders();
256 |
257 | test.equal(mc._getHeader("in-reply-to"), "");
258 | test.equal(mc._getHeader("references"), " ");
259 | test.done();
260 | },
261 |
262 | "Convert addresses": function(test){
263 | var mc = new MailComposer(),
264 | input = [
265 | {
266 | name: "I \"am\" test",
267 | address: "test@example.com"
268 | },
269 | {
270 | name: "Punycode Address",
271 | address: "test@jõgeva.com"
272 | },
273 | {
274 | address: "test@example.com"
275 | }
276 | ],
277 | output = '"I \\"am\\" test" , "Punycode Address" , test@example.com';
278 |
279 | test.deepEqual(mc._convertAddresses(input), output);
280 |
281 | test.done();
282 | }
283 | };
284 |
285 |
286 | exports["Text encodings"] = {
287 | "Punycode": function(test){
288 | test.equal(toPunycode("andris@age.ee"), "andris@age.ee");
289 | test.equal(toPunycode("andris@äge.ee"), "andris@xn--ge-uia.ee");
290 | test.done();
291 | },
292 |
293 | "Mime words": function(test){
294 | var mc = new MailComposer();
295 | test.equal(mc._encodeMimeWord("Tere"), "Tere");
296 | test.equal(mc._encodeMimeWord("Tere","Q"), "Tere");
297 | test.equal(mc._encodeMimeWord("Tere","B"), "Tere");
298 |
299 | // simple
300 | test.equal(mc._encodeMimeWord("äss"), "=?UTF-8?Q?=C3=A4ss?=");
301 | test.equal(mc._encodeMimeWord("äss","B"), "=?UTF-8?B?"+(new Buffer("äss","utf-8").toString("base64"))+"?=");
302 |
303 | //multiliple
304 | test.equal(mc._encodeMimeWord("äss tekst on see siin või kuidas?","Q", 20), "=?UTF-8?Q?=C3=A4ss?= tekst on see siin =?UTF-8?Q?v=C3=B5i?= kuidas?");
305 |
306 | test.done();
307 | },
308 |
309 | "Addresses": function(test){
310 | var mc = new MailComposer();
311 | mc.setMessageOption({
312 | sender: '"Jaanuar Veebruar, Märts" '
313 | });
314 |
315 | test.equal(mc._message.from, "\"Jaanuar Veebruar, =?UTF-8?Q?M=C3=A4rts?=\" <=?UTF-8?Q?m=C3=A4rts?=@xn--mrts-loa.eu>");
316 |
317 | mc.setMessageOption({
318 | sender: 'aavik '
319 | });
320 |
321 | test.equal(mc._message.from, '"aavik" ');
322 |
323 | mc.setMessageOption({
324 | sender: ''
325 | });
326 |
327 | test.equal(mc._message.from, 'aavik@node.ee');
328 |
329 | mc.setMessageOption({
330 | sender: ''
331 | });
332 |
333 | test.equal(mc._message.from, 'aavik@xn--mrts-loa.eu');
334 |
335 | // multiple
336 |
337 | mc.setMessageOption({
338 | sender: ', juulius@node.ee, "Node, Master" '
339 | });
340 |
341 | test.equal(mc._message.from, 'aavik@xn--mrts-loa.eu, juulius@node.ee, "Node, Master" ');
342 |
343 | mc.setMessageOption({
344 | sender: ['', 'juulius@node.ee, "Node, Master" ', 'andris@node.ee']
345 | });
346 |
347 | mc.setMessageOption({
348 | to: ['', 'juulius@node.ee, "Node, Master" ', 'andris@node.ee']
349 | });
350 |
351 | test.equal(mc._message.from, 'aavik@xn--mrts-loa.eu, juulius@node.ee, "Node, Master" , andris@node.ee');
352 | test.equal(mc._message.to, 'aavik@xn--mrts-loa.eu, juulius@node.ee, "Node, Master" , andris@node.ee');
353 |
354 | test.done();
355 | },
356 |
357 | "Invalid subject": function(test){
358 | var mc = new MailComposer();
359 | mc.setMessageOption({
360 | subject: "tere\ntere!"
361 | });
362 |
363 | test.equal(mc._message.subject, "tere tere!");
364 | test.done();
365 | },
366 |
367 | "Long header line": function(test){
368 | var mc = new MailComposer();
369 |
370 | mc._headers = {
371 | From: "a very log line, \"=?UTF-8?Q?Jaanuar_Veebruar,_M=C3=A4rts?=\" <=?UTF-8?Q?m=C3=A4rts?=@xn--mrts-loa.eu>"
372 | };
373 |
374 | mc.on("data", function(chunk){
375 | test.ok(chunk.toString().trim().match(/From\:\s[^\r\n]+\r\n\s+[^\r\n]+/));
376 | test.done();
377 | });
378 | mc._composeHeader();
379 |
380 | }
381 |
382 | };
383 |
384 | exports["Mail related"] = {
385 | "Envelope": function(test){
386 | var mc = new MailComposer();
387 | mc.setMessageOption({
388 | sender: '"Jaanuar Veebruar, Märts" ',
389 | to: ', juulius@node.ee',
390 | cc: '"Node, Master" ',
391 | bcc:'Undisclosed Recipients:ahven@tr.ee, "Mäger" ;'
392 | });
393 |
394 | test.deepEqual(mc._envelope, {from:[ 'märts@xn--mrts-loa.eu' ],to:[ 'aavik@xn--mrts-loa.eu', 'juulius@node.ee'], cc:['node@node.ee' ], bcc: [ 'ahven@tr.ee', 'mager@xn--mger-loa.ee' ]});
395 | test.done();
396 | },
397 |
398 | "User defined envelope": function(test){
399 | var mc = new MailComposer();
400 | mc.setMessageOption({
401 | sender: '"Jaanuar Veebruar, Märts" ',
402 | envelope: {
403 | from: "Andris ",
404 | to: ["Andris , Node ", "aavik@märts.eu", "juulius@gmail.com"],
405 | cc: "trips@node.ee"
406 | },
407 | to: ', juulius@node.ee',
408 | cc: '"Node, Master" '
409 | });
410 |
411 | test.deepEqual(mc._envelope, {userDefined: true, from:[ 'andris@tr.ee' ],to:[ 'andris@tr.ee', 'andris@node.ee', 'aavik@xn--mrts-loa.eu', 'juulius@gmail.com'], "cc":['trips@node.ee']});
412 | test.done();
413 | },
414 |
415 | "Add alternative": function(test){
416 | var mc = new MailComposer();
417 | mc.addAlternative();
418 | test.equal(mc._alternatives.length, 0);
419 |
420 | mc.addAlternative({contents:"tere tere"});
421 | test.equal(mc._alternatives.length, 1);
422 |
423 | test.equal(mc._alternatives[0].contentType, "application/octet-stream");
424 | test.equal(mc._alternatives[0].contentEncoding, "base64");
425 | test.equal(mc._alternatives[0].contents, "tere tere");
426 |
427 | mc.addAlternative({contents:"tere tere", contentType:"text/plain", contentEncoding:"7bit"});
428 | test.equal(mc._alternatives[1].contentType, "text/plain");
429 | test.equal(mc._alternatives[1].contentEncoding, "7bit");
430 |
431 | test.done();
432 | },
433 |
434 | "Add attachment": function(test){
435 | var mc = new MailComposer();
436 | mc.addAttachment();
437 | test.equal(mc._attachments.length, 0);
438 |
439 | mc.addAttachment({filePath:"/tmp/var.txt"});
440 | test.equal(mc._attachments[0].contentType, "text/plain");
441 | test.equal(mc._attachments[0].fileName, "var.txt");
442 |
443 | mc.addAttachment({contents:"/tmp/var.txt"});
444 | test.equal(mc._attachments[1].contentType, "application/octet-stream");
445 | test.equal(mc._attachments[1].fileName, undefined);
446 |
447 | mc.addAttachment({filePath:"/tmp/var.txt", fileName:"test.txt"});
448 | test.equal(mc._attachments[2].fileName, "test.txt");
449 |
450 | test.done();
451 | },
452 |
453 | "Default attachment disposition": function(test){
454 | var mc = new MailComposer();
455 | mc.addAttachment();
456 | test.equal(mc._attachments.length, 0);
457 |
458 | mc.addAttachment({filePath:"/tmp/var.txt"});
459 | test.equal(mc._attachments[0].contentDisposition, undefined);
460 |
461 | test.done();
462 | },
463 |
464 | "Set attachment disposition": function(test){
465 | var mc = new MailComposer();
466 | mc.addAttachment();
467 | test.equal(mc._attachments.length, 0);
468 |
469 | mc.addAttachment({filePath:"/tmp/var.txt", contentDisposition: "inline"});
470 | test.equal(mc._attachments[0].contentDisposition, "inline");
471 |
472 | test.done();
473 | },
474 |
475 | "Generate envelope": function(test){
476 | var mc = new MailComposer();
477 | mc.setMessageOption({
478 | sender: '"Jaanuar Veebruar, Märts" , karu@ahven.ee',
479 | to: ', juulius@node.ee',
480 | cc: '"Node, Master" '
481 | });
482 |
483 | test.deepEqual(mc.getEnvelope(), {from: 'märts@xn--mrts-loa.eu',to:[ 'aavik@xn--mrts-loa.eu', 'juulius@node.ee', 'node@node.ee' ], stamp: 'Postage paid, Par Avion'});
484 | test.done();
485 | },
486 |
487 | "Generate user defined envelope": function(test){
488 | var mc = new MailComposer();
489 | mc.setMessageOption({
490 | sender: '"Jaanuar Veebruar, Märts" , karu@ahven.ee',
491 | to: ', juulius@node.ee',
492 | envelope: {
493 | from: "Andris ",
494 | to: ["Andris , Node ", "aavik@märts.eu", "juulius@gmail.com"],
495 | cc: "trips@node.ee"
496 | },
497 | cc: '"Node, Master" '
498 | });
499 |
500 | test.deepEqual(mc.getEnvelope(), {from: 'andris@tr.ee', to:[ 'andris@tr.ee', 'andris@node.ee', 'aavik@xn--mrts-loa.eu', 'juulius@gmail.com', 'trips@node.ee'], stamp: 'Postage paid, Par Avion'});
501 | test.done();
502 | },
503 |
504 | "Generate Headers": function(test){
505 | var mc = new MailComposer();
506 | mc.setMessageOption({
507 | sender: '"Jaanuar Veebruar, Märts" , karu@ahven.ee',
508 | to: ', juulius@node.ee',
509 | cc: '"Node, Master" , koger@mäger.ee, Undisclosed Recipients:ahven@tr.ee, "Mäger" ;',
510 | replyTo: 'julla@pulla.ee',
511 | subject: "Tere õkva!"
512 | });
513 |
514 | mc.on("data", function(chunk){
515 | chunk = (chunk || "").toString("utf-8");
516 | test.ok(chunk.match(/^(?:(?:[\s]+|[a-zA-Z0-0\-]+\:)[^\r\n]+\r\n)+\r\n$/));
517 | test.done();
518 | });
519 |
520 | mc._composeHeader();
521 | }
522 | };
523 |
524 | exports["Mime tree"] = {
525 | "No contents": function(test){
526 | test.expect(4);
527 |
528 | var mc = new MailComposer();
529 | mc._composeMessage();
530 |
531 | test.ok(!mc._message.tree.boundary);
532 | test.equal(mc._getHeader("Content-Type").split(";").shift().trim(), "text/plain");
533 | test.equal(mc._message.tree.childNodes.length, 0);
534 |
535 | for(var i=0, len = mc._message.flatTree.length; itest"
570 | });
571 | mc._composeMessage();
572 |
573 | test.ok(!mc._message.tree.boundary);
574 | test.equal(mc._getHeader("Content-Type").split(";").shift().trim(), "text/html");
575 | test.equal(mc._message.tree.childNodes.length, 0);
576 |
577 | for(var i=0, len = mc._message.flatTree.length; itest");
580 | }
581 | }
582 |
583 | test.done();
584 | },
585 | "HTML and text contents": function(test){
586 | test.expect(5);
587 |
588 | var mc = new MailComposer();
589 | mc.setMessageOption({
590 | body: "test",
591 | html: "test"
592 | });
593 | mc._composeMessage();
594 |
595 | test.equal(mc._message.tree.childNodes.length, 2);
596 | test.equal(mc._getHeader("Content-Type").split(";").shift().trim(), "multipart/alternative");
597 | test.ok(mc._message.tree.boundary);
598 |
599 | for(var i=0, len = mc._message.flatTree.length; itest"
800 | });
801 | mc.streamMessage();
802 |
803 | var mp = new MailParser();
804 |
805 | mc.pipe(mp);
806 |
807 | mp.on("end", function(mail){
808 | test.equal(mail.html.trim(), "test");
809 | test.done();
810 | });
811 | },
812 | "HTML and text": function(test){
813 | var mc = new MailComposer();
814 | mc.setMessageOption({
815 | html: "test",
816 | body: "test"
817 | });
818 | mc.streamMessage();
819 |
820 | var mp = new MailParser();
821 |
822 | mc.pipe(mp);
823 |
824 | mp.on("end", function(mail){
825 | test.equal(mail.text.trim(), "test");
826 | test.equal(mail.html.trim(), "test");
827 | test.done();
828 | });
829 | },
830 | "Flowed text": function(test){
831 | var mc = new MailComposer({encoding:"8bit"}),
832 | file = fs.readFileSync(__dirname+"/textfile.txt").toString("utf-8");
833 |
834 | mc.setMessageOption({
835 | body: file
836 | });
837 | mc.streamMessage();
838 |
839 | var mp = new MailParser();
840 |
841 | mc.pipe(mp);
842 |
843 | mp.on("end", function(mail){
844 | test.equal(mail.text.trim(), file.trim());
845 | test.done();
846 | });
847 | },
848 | "Attachment as string": function(test){
849 | var mc = new MailComposer();
850 | mc.setMessageOption();
851 | mc.addAttachment({
852 | fileName: "file.txt",
853 | contents: fs.readFileSync(__dirname+"/textfile.txt").toString("utf-8")
854 | });
855 | mc.streamMessage();
856 |
857 | var mp = new MailParser();
858 |
859 | mc.pipe(mp);
860 |
861 | mp.on("end", function(mail){
862 | test.equal(mail.attachments[0].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
863 | test.done();
864 | });
865 | },
866 | "Attachment as buffer": function(test){
867 | var mc = new MailComposer();
868 | mc.setMessageOption();
869 | mc.addAttachment({
870 | fileName: "file.txt",
871 | contents: fs.readFileSync(__dirname+"/textfile.txt")
872 | });
873 | mc.streamMessage();
874 |
875 | var mp = new MailParser();
876 |
877 | mc.pipe(mp);
878 |
879 | mp.on("end", function(mail){
880 | test.equal(mail.attachments[0].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
881 | test.done();
882 | });
883 | },
884 | "Attachment file stream": function(test){
885 | var mc = new MailComposer();
886 | mc.setMessageOption();
887 | mc.addAttachment({
888 | fileName: "file.txt",
889 | filePath: __dirname+"/textfile.txt"
890 | });
891 | mc.streamMessage();
892 |
893 | var mp = new MailParser();
894 |
895 | mc.pipe(mp);
896 |
897 | mp.on("end", function(mail){
898 | test.equal(mail.attachments[0].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
899 | test.done();
900 | });
901 | },
902 | "Attachment source stream": function(test){
903 | var mc = new MailComposer();
904 |
905 | var fileStream = fs.createReadStream(__dirname+"/textfile.txt");
906 |
907 | mc.setMessageOption();
908 | mc.addAttachment({
909 | fileName: "file.txt",
910 | streamSource: fileStream
911 | });
912 | mc.streamMessage();
913 |
914 | var mp = new MailParser();
915 |
916 | mc.pipe(mp);
917 |
918 | mp.on("end", function(mail){
919 | test.equal(mail.attachments[0].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
920 | test.done();
921 | });
922 | },
923 | "Attachment source url": function(test){
924 |
925 | var server = http.createServer(function (req, res) {
926 | if(req.url=="/textfile.txt"){
927 | fs.createReadStream(__dirname+"/textfile.txt").pipe(res);
928 | }else{
929 | res.writeHead(404, {'Content-Type': 'text/plain'});
930 | res.end('Not found!\n');
931 | }
932 | });
933 | server.listen(HTTP_PORT, '127.0.0.1');
934 |
935 | var mc = new MailComposer();
936 |
937 | mc.setMessageOption();
938 | mc.addAttachment({
939 | fileName: "file.txt",
940 | filePath: "http://localhost:"+HTTP_PORT+"/textfile.txt"
941 | });
942 | mc.streamMessage();
943 |
944 | var mp = new MailParser();
945 |
946 | mc.pipe(mp);
947 |
948 | mp.on("end", function(mail){
949 | test.equal(mail.attachments[0].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
950 | server.close();
951 | test.done();
952 | });
953 | },
954 | "Attachment source invalid url": function(test){
955 |
956 | var server = http.createServer(function (req, res) {
957 | res.writeHead(404, {'Content-Type': 'text/plain'});
958 | res.end('Not found!\n');
959 | });
960 | server.listen(HTTP_PORT, '127.0.0.1');
961 |
962 | var mc = new MailComposer();
963 |
964 | mc.setMessageOption();
965 | mc.addAttachment({
966 | fileName: "file.txt",
967 | filePath: "http://localhost:"+HTTP_PORT+"/textfile.txt"
968 | });
969 | mc.streamMessage();
970 |
971 | var mp = new MailParser();
972 |
973 | mc.pipe(mp);
974 |
975 | mp.on("end", function(mail){
976 | test.equal(mail.attachments[0].checksum, "c47d65ffb34285e04a793dea442895b8");
977 | server.close();
978 | test.done();
979 | });
980 | },
981 | "Custom User-Agent": function(test){
982 |
983 | var server = http.createServer(function (req, res) {
984 | test.equal(req.headers['user-agent'], "test");
985 |
986 | res.writeHead(200, {'Content-Type': 'text/plain'});
987 | res.end('OK!\n');
988 | })
989 | server.listen(HTTP_PORT, '127.0.0.1');
990 |
991 | var mc = new MailComposer();
992 |
993 | mc.setMessageOption();
994 | mc.addAttachment({
995 | fileName: "file.txt",
996 | filePath: "http://localhost:"+HTTP_PORT+"/textfile.txt",
997 | userAgent: "test"
998 | });
999 | mc.streamMessage();
1000 |
1001 | var mp = new MailParser();
1002 |
1003 | mc.pipe(mp);
1004 |
1005 | mp.on("end", function(mail){
1006 | server.close();
1007 | test.done();
1008 | });
1009 | },
1010 | "escape SMTP": function(test){
1011 | var mc = new MailComposer({escapeSMTP: true});
1012 | mc.setMessageOption({
1013 | body: ".\r\n."
1014 | });
1015 | mc.streamMessage();
1016 |
1017 | var mp = new MailParser();
1018 |
1019 | mc.pipe(mp);
1020 |
1021 | mp.on("end", function(mail){
1022 | test.equal(mail.text.trim(), "..\n..");
1023 | test.done();
1024 | });
1025 | },
1026 | "don't escape SMTP": function(test){
1027 | var mc = new MailComposer({escapeSMTP: false});
1028 | mc.setMessageOption({
1029 | body: ".\r\n."
1030 | });
1031 | mc.streamMessage();
1032 |
1033 | var mp = new MailParser();
1034 |
1035 | mc.pipe(mp);
1036 |
1037 | mp.on("end", function(mail){
1038 | test.equal(mail.text.trim(), ".\n.");
1039 | test.done();
1040 | });
1041 | },
1042 | "HTML and text and attachment": function(test){
1043 | var mc = new MailComposer();
1044 | mc.setMessageOption({
1045 | html: "test",
1046 | body: "test"
1047 | });
1048 | mc.addAttachment({
1049 | fileName: "file.txt",
1050 | contents: fs.readFileSync(__dirname+"/textfile.txt").toString("utf-8")
1051 | });
1052 | mc.streamMessage();
1053 |
1054 | var mp = new MailParser();
1055 |
1056 | mc.pipe(mp);
1057 |
1058 | mp.on("end", function(mail){
1059 | test.equal(mail.text.trim(), "test");
1060 | test.equal(mail.html.trim(), "test");
1061 | test.equal(mail.attachments[0].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
1062 | test.done();
1063 | });
1064 | },
1065 | "HTML and related attachment": function(test){
1066 | var mc = new MailComposer();
1067 | mc.setMessageOption({
1068 | html: "
"
1069 | });
1070 | mc.addAttachment({
1071 | fileName: "file.txt",
1072 | cid: "test@node",
1073 | contents: fs.readFileSync(__dirname+"/textfile.txt").toString("utf-8")
1074 | });
1075 | mc.streamMessage();
1076 |
1077 | var mp = new MailParser();
1078 |
1079 | mc.pipe(mp);
1080 | /*
1081 | var d = "";
1082 | mc.on("data", function(data){
1083 | d += data.toString();
1084 | })
1085 |
1086 | mc.on("end", function(){
1087 | console.log(d);
1088 | });
1089 | */
1090 |
1091 | mp.on("end", function(mail){
1092 | test.equal(mc._attachments.length, 0);
1093 | test.equal(mc._relatedAttachments.length, 1);
1094 | test.equal(mail.html.trim(), "
");
1095 | test.equal(mail.attachments[0].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
1096 | test.done();
1097 | });
1098 | },
1099 | "HTML and related plus regular attachment": function(test){
1100 | var mc = new MailComposer();
1101 | mc.setMessageOption({
1102 | html: "
"
1103 | });
1104 | mc.addAttachment({
1105 | fileName: "file.txt",
1106 | cid: "test@node",
1107 | contents: fs.readFileSync(__dirname+"/textfile.txt").toString("utf-8")
1108 | });
1109 | mc.addAttachment({
1110 | fileName: "file.txt",
1111 | contents: fs.readFileSync(__dirname+"/textfile.txt").toString("utf-8")
1112 | });
1113 | mc.streamMessage();
1114 |
1115 | var mp = new MailParser();
1116 |
1117 | mc.pipe(mp);
1118 |
1119 | mp.on("end", function(mail){
1120 | test.equal(mc._attachments.length, 1);
1121 | test.equal(mc._relatedAttachments.length, 1);
1122 | test.equal(mail.html.trim(), "
");
1123 | test.equal(mail.attachments[0].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
1124 | test.equal(mail.attachments[1].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
1125 | test.done();
1126 | });
1127 | },
1128 | "HTML and text related attachment": function(test){
1129 | var mc = new MailComposer();
1130 | mc.setMessageOption({
1131 | html: "
",
1132 | text:"test"
1133 | });
1134 | mc.addAttachment({
1135 | fileName: "file.txt",
1136 | cid: "test@node",
1137 | contents: fs.readFileSync(__dirname+"/textfile.txt").toString("utf-8")
1138 | });
1139 | mc.streamMessage();
1140 |
1141 | var mp = new MailParser();
1142 |
1143 | mc.pipe(mp);
1144 |
1145 | mp.on("end", function(mail){
1146 | test.equal(mc._attachments.length, 0);
1147 | test.equal(mc._relatedAttachments.length, 1);
1148 | test.equal(mail.text.trim(), "test");
1149 | test.equal(mail.html.trim(), "
");
1150 | test.equal(mail.attachments[0].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
1151 | test.done();
1152 | });
1153 | },
1154 | "HTML, text, related+regular attachment": function(test){
1155 | var mc = new MailComposer();
1156 | mc.setMessageOption({
1157 | html: "
",
1158 | text:"test"
1159 | });
1160 | mc.addAttachment({
1161 | fileName: "file.txt",
1162 | cid: "test@node",
1163 | contents: fs.readFileSync(__dirname+"/textfile.txt").toString("utf-8")
1164 | });
1165 | mc.addAttachment({
1166 | fileName: "file.txt",
1167 | contents: fs.readFileSync(__dirname+"/textfile.txt").toString("utf-8")
1168 | });
1169 | mc.streamMessage();
1170 |
1171 | var mp = new MailParser();
1172 |
1173 | mc.pipe(mp);
1174 |
1175 | mp.on("end", function(mail){
1176 | test.equal(mc._attachments.length, 1);
1177 | test.equal(mc._relatedAttachments.length, 1);
1178 | test.equal(mail.text.trim(), "test");
1179 | test.equal(mail.html.trim(), "
");
1180 | test.equal(mail.attachments[0].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
1181 | test.equal(mail.attachments[1].checksum, "59fbcbcaf18cb9232f7da6663f374eb9");
1182 | test.done();
1183 | });
1184 | },
1185 | "Only alternative": function(test){
1186 | var mc = new MailComposer();
1187 | mc.addAlternative({
1188 | contents: "tere tere"
1189 | });
1190 | mc.streamMessage();
1191 |
1192 | var mp = new MailParser();
1193 |
1194 | mc.pipe(mp);
1195 |
1196 | mp.on("end", function(mail){
1197 | test.equal(mail.attachments.length, 1)
1198 | test.equal(mail.attachments[0].content.toString(), "tere tere")
1199 | test.equal(mail.attachments[0].contentType, "application/octet-stream")
1200 | test.done();
1201 | });
1202 | },
1203 | "References Header": function(test){
1204 |
1205 | var mc = new MailComposer();
1206 | mc.setMessageOption({
1207 | references: ["myrdo", "vyrdo"]
1208 | });
1209 | mc.streamMessage();
1210 |
1211 | var mp = new MailParser();
1212 |
1213 | mc.pipe(mp);
1214 |
1215 | mp.on("end", function(mail){
1216 | test.deepEqual(mail.references, ["myrdo", "vyrdo"]);
1217 | test.done();
1218 | });
1219 | },
1220 | "InReplyTo Header": function(test){
1221 |
1222 | var mc = new MailComposer();
1223 | mc.setMessageOption({
1224 | inReplyTo: "test"
1225 | });
1226 | mc.streamMessage();
1227 |
1228 | var mp = new MailParser();
1229 |
1230 | mc.pipe(mp);
1231 |
1232 | mp.on("end", function(mail){
1233 | test.equal(mail.inReplyTo, "test");
1234 | test.done();
1235 | });
1236 | },
1237 | "Non UTF-8 charset": function(test){
1238 | var mc = new MailComposer({charset: "iso-8859-1"}),
1239 | message = "",
1240 | mp = new MailParser(),
1241 | subject = "Jõgeva maakond, Ärni küla",
1242 | text = "Lõäöpõld Jääger Sußi",
1243 | fromName = "Mäger Mõksi",
1244 | from = fromName+" "
1245 |
1246 | mc.setMessageOption({
1247 | subject: subject,
1248 | text: text,
1249 | from: from
1250 | });
1251 |
1252 | mp.on("end", function(mail){
1253 | //console.log(mail);
1254 | test.equal(mail.subject, subject);
1255 | test.equal((mail.text || "").trim(), text);
1256 | test.equal(fromName, mail.from && mail.from[0] && mail.from[0].name);
1257 | test.done();
1258 | });
1259 |
1260 | mc.on("data", function(chunk){
1261 | message += chunk.toString("utf-8");
1262 | });
1263 |
1264 | mc.on("end", function(){
1265 | //console.log(message)
1266 | test.ok(message.match(/J=F5geva/));
1267 | test.ok(message.match(/=C4rni_k=FCla/));
1268 | test.ok(message.match(/L=F5=E4=F6p=F5ld J=E4=E4ger Su=DFi/));
1269 | test.ok(message.match(/M=E4ger_M=F5ksi/));
1270 |
1271 | mp.end(message);
1272 | });
1273 | mc.streamMessage();
1274 | },
1275 | "Convert image URL to embedded attachment": function(test){
1276 |
1277 | var image1 = new Buffer("iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==", "base64"),
1278 | image2 = new Buffer("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC", "base64");
1279 |
1280 | var server = http.createServer(function (req, res) {
1281 | if(req.url=="/image1.png"){
1282 | res.writeHead(200, {'Content-Type': 'image/png'});
1283 | res.end(image1);
1284 | }else if(req.url=="/image2.png"){
1285 | res.writeHead(200, {'Content-Type': 'image/png'});
1286 | res.end(image2);
1287 | }else if(req.url=="/a=1&b=2"){
1288 | res.writeHead(200, {'Content-Type': 'image/png'});
1289 | res.end(image2);
1290 | }else{
1291 | res.writeHead(404, {'Content-Type': 'text/plain'});
1292 | res.end('Not found!\n');
1293 | }
1294 | });
1295 | server.listen(HTTP_PORT, '127.0.0.1');
1296 |
1297 | var mc = new MailComposer({
1298 | forceEmbeddedImages: true
1299 | });
1300 |
1301 | mc.setMessageOption({
1302 | from: "andris@kreata.ee",
1303 | to: "andris@node.ee",
1304 | subject: "embedded images",
1305 | html: 'Embedded images:
\n'+
1306 | '\n'+
1307 | ' - Embedded image1

\n'+
1308 | ' - Embedded image2

\n'+
1309 | ' - Embedded image3

\n'+
1310 | ' - Embedded image4

\n'+
1311 | '
'
1312 | });
1313 | mc.streamMessage();
1314 |
1315 | var mp = new MailParser();
1316 |
1317 | mc.pipe(mp);
1318 |
1319 | var str = "";
1320 | mc.on("data", function(chunk){str += chunk.toString()})
1321 |
1322 | mp.on("end", function(mail){
1323 | test.equal(mail.attachments[0].content.toString("base64"), image1.toString("base64"));
1324 | test.equal(mail.attachments[1].content.toString("base64"), image2.toString("base64"));
1325 | test.equal(mail.attachments[2].checksum, "29445222b4f912167463b8c65e9a6420");
1326 | test.equal(mail.attachments[3].content.toString("base64"), image2.toString("base64"));
1327 | server.close();
1328 | test.done();
1329 | });
1330 | }
1331 | };
1332 |
1333 | exports["Output buffering"] = {
1334 | "Use DKIM": function(test){
1335 | var mc = new MailComposer();
1336 |
1337 | mc.setMessageOption({
1338 | from: "Andris Reinman ",
1339 | to: "Andris ",
1340 | html: "Hello world!",
1341 | subject: "Hello world!"
1342 | });
1343 |
1344 | mc.useDKIM({
1345 | domainName: "do-not-trust.node.ee",
1346 | keySelector: "dkim",
1347 | privateKey: fs.readFileSync(__dirname+"/test_private.pem")
1348 | });
1349 |
1350 | mc.streamMessage();
1351 |
1352 | var mp = new MailParser();
1353 |
1354 | mc.pipe(mp);
1355 |
1356 | mp.on("end", function(mail){
1357 | test.equal(mail.headers['dkim-signature'].replace(/\s/g, ""), 'v=1;a=rsa-sha256;c=relaxed/relaxed;d=do-not-trust.node.ee;q=dns/txt;s=dkim;bh=88i0PUP3tj3X/n0QT6Baw8ZPSeHZPqT7J0EmE26pjng=;h=from:subject:to:mime-version:content-type:content-transfer-encoding;b=dtxxQLotrcarEA5nbgBJLBJQxSAHcfrNxxpItcXSj68ntRvxmjXt9aPZTbVrzfRYe+xRzP2FTGpS7js8iYpAZZ2N3DBRLVp4gyyKHB1oWMkg/EV92uPtnjQ3MlHMbxC0');
1358 | test.done();
1359 | });
1360 | },
1361 |
1362 | "Use DKIM with escapeSMTP": function(test){
1363 | var mc = new MailComposer({escapeSMTP: true});
1364 |
1365 | mc.setMessageOption({
1366 | from: "Andris Reinman ",
1367 | to: "Andris ",
1368 | text: ".Hello World",
1369 | subject: "Hello world!"
1370 | });
1371 |
1372 | mc.useDKIM({
1373 | domainName: "do-not-trust.node.ee",
1374 | keySelector: "dkim",
1375 | privateKey: fs.readFileSync(__dirname+"/test_private.pem")
1376 | });
1377 |
1378 | mc.streamMessage();
1379 |
1380 | var mp = new MailParser();
1381 |
1382 | mc.pipe(mp);
1383 |
1384 | mp.on("end", function(mail){
1385 | test.equal(mail.headers['dkim-signature'].replace(/\s/g, ""), 'v=1;a=rsa-sha256;c=relaxed/relaxed;d=do-not-trust.node.ee;q=dns/txt;s=dkim;bh=G6F+AsXwSI9QevjP2K03mJc6ftXnKHR5egjeWd29Muo=;h=from:subject:to:mime-version:content-type:content-transfer-encoding;b=GY96OWF1PPjzRDpw/QSEz7OfbW/MWb4PO0nA6PJNtdpAPSUJMomz4klv99rrqW8z8xW1ha9LM+39EPUN7c29OTNPoRJ9ybb9F1lttfD0l7AETFboBEknrdMaouc+HYWA');
1386 | test.done();
1387 | });
1388 | },
1389 |
1390 | "Build message": function(test){
1391 | var mc = new MailComposer();
1392 |
1393 | mc.setMessageOption({
1394 | from: "Andris Reinman ",
1395 | to: "Andris ",
1396 | html: "Hello world!",
1397 | subject: "Hello world!"
1398 | });
1399 |
1400 | mc.useDKIM({
1401 | domainName: "do-not-trust.node.ee",
1402 | keySelector: "dkim",
1403 | privateKey: fs.readFileSync(__dirname+"/test_private.pem")
1404 | });
1405 |
1406 | mc.buildMessage(function(err, body){
1407 | test.ifError(err);
1408 | var mp = new MailParser();
1409 |
1410 | mp.on("end", function(mail){
1411 | test.equal(mail.headers['dkim-signature'].replace(/\s/g, ""), 'v=1;a=rsa-sha256;c=relaxed/relaxed;d=do-not-trust.node.ee;q=dns/txt;s=dkim;bh=88i0PUP3tj3X/n0QT6Baw8ZPSeHZPqT7J0EmE26pjng=;h=from:subject:to:mime-version:content-type:content-transfer-encoding;b=dtxxQLotrcarEA5nbgBJLBJQxSAHcfrNxxpItcXSj68ntRvxmjXt9aPZTbVrzfRYe+xRzP2FTGpS7js8iYpAZZ2N3DBRLVp4gyyKHB1oWMkg/EV92uPtnjQ3MlHMbxC0');
1412 | test.done();
1413 | });
1414 |
1415 | mp.end(body);
1416 | });
1417 |
1418 |
1419 | }
1420 | };
1421 |
--------------------------------------------------------------------------------
/lib/mailcomposer.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var Stream = require("stream").Stream,
4 | utillib = require("util"),
5 | mimelib = require("mimelib"),
6 | he = require("he"),
7 | toPunycode = require("./topunycode"),
8 | runDKIMSign = require("dkim-signer").DKIMSign,
9 | urlFetch = require("./urlfetch"),
10 | fs = require("fs"),
11 | mime = require("mime"),
12 | crypto = require("crypto");
13 |
14 | module.exports.MailComposer = MailComposer;
15 |
16 | /**
17 | * Costructs a MailComposer object. This is a Stream instance so you could
18 | * pipe the output to a file or send it to network.
19 | *
20 | * Possible options properties are:
21 | *
22 | *
23 | * - escapeSMTP - convert dots in the beginning of line to double dots
24 | * - encoding - forced transport encoding (quoted-printable, base64, 7bit or 8bit)
25 | * - charset - forced output charset (utf-8, iso-8859-1 etc)
26 | * - keepBcc - include Bcc: field in the message headers (default is false)
27 | * - forceEmbeddedImages - convert image urls and absolute paths in HTML to embedded attachments (default is false)
28 | *
29 | *
30 | * Events
31 | *
32 | *
33 | * - 'envelope' - emits an envelope object with
from and to (array) addresses.
34 | * - 'data' - emits a chunk of data
35 | * - 'end' - composing the message has ended
36 | *
37 | *
38 | * @constructor
39 | * @param {Object} [options] Optional options object
40 | */
41 | function MailComposer(options){
42 | Stream.call(this);
43 |
44 | this.readable = true;
45 |
46 | this.options = options || {};
47 | this.options.charset = (this.options.charset || "utf-8").toString().trim().toLowerCase();
48 | this.options.identityString = (this.options.identityString || "mailcomposer").toString().trim().replace(/\s/g,"-");
49 |
50 | this._lineEnding="\r\n";
51 |
52 | this._init();
53 | }
54 | utillib.inherits(MailComposer, Stream);
55 |
56 | /**
57 | * Resets and initializes MailComposer
58 | */
59 | MailComposer.prototype._init = function(){
60 | /**
61 | * Contains all header values
62 | * @private
63 | */
64 | this._headers = {};
65 |
66 | /**
67 | * Contains message related values
68 | * @private
69 | */
70 | this._message = {};
71 |
72 | /**
73 | * Contains a list of alternatives for text and html body
74 | * @private
75 | */
76 | this._alternatives = [];
77 |
78 | /**
79 | * Contains a list of attachments
80 | * @private
81 | */
82 | this._attachments = [];
83 |
84 | /**
85 | * Contains a list of attachments that are related to HTML body
86 | * @private
87 | */
88 | this._relatedAttachments = [];
89 |
90 | /**
91 | * Contains e-mail addresses for the SMTP
92 | * @private
93 | */
94 | this._envelope = {};
95 |
96 | /**
97 | * If set to true, caches the output for further processing (DKIM signing etc.)
98 | * @private
99 | */
100 | this._cacheOutput = false;
101 |
102 | /**
103 | * If _cacheOutput is true, caches the output to _outputBuffer
104 | * @private
105 | */
106 | this._outputBuffer = "";
107 |
108 | /**
109 | * DKIM message signing options, set with useDKIM
110 | * @private
111 | */
112 | this._dkim = false;
113 |
114 | /**
115 | * Current stream being processed. Needed for directing backpressure
116 | * @private
117 | */
118 | this._currentStream = false;
119 |
120 | /**
121 | * Counter for generating unique mime boundaries etc.
122 | * @private
123 | */
124 | this._gencounter = 0;
125 | };
126 |
127 | /* PUBLIC API */
128 |
129 | /**
130 | * Adds a header field to the headers object
131 | *
132 | * @param {String} key Key name
133 | * @param {String} value Header value
134 | * @param {Boolean} [formatted] If set to true, the value is not modified and passed to output as is
135 | */
136 | MailComposer.prototype.addHeader = function(key, value, formatted){
137 | key = this._normalizeKey(key);
138 |
139 | if(value && Object.prototype.toString.call(value) == "[object Object]"){
140 | value = this._encodeMimeWord(JSON.stringify(value), "Q", 52);
141 | }else{
142 | value = (value || "").toString().trim();
143 | }
144 |
145 | if(!key || !value){
146 | return;
147 | }
148 |
149 | if(formatted){
150 | value = {
151 | formatted: !!formatted,
152 | value: value
153 | };
154 | }
155 |
156 | if(!(key in this._headers)){
157 | this._headers[key] = value;
158 | }else{
159 | if(!Array.isArray(this._headers[key])){
160 | this._headers[key] = [this._headers[key], value];
161 | }else{
162 | this._headers[key].push(value);
163 | }
164 | }
165 | };
166 |
167 | /**
168 | * Resets and initializes MailComposer
169 | *
170 | * Setting an option overwrites an earlier setup for the same keys
171 | *
172 | * Possible options:
173 | *
174 | *
175 | * - from - The e-mail address of the sender. All e-mail addresses can be plain
sender@server.com or formatted Sender Name <sender@server.com>
176 | * - to - Comma separated list of recipients e-mail addresses that will appear on the
To: field
177 | * - cc - Comma separated list of recipients e-mail addresses that will appear on the
Cc: field
178 | * - bcc - Comma separated list of recipients e-mail addresses that will appear on the
Bcc: field
179 | * - replyTo - An e-mail address that will appear on the
Reply-To: field
180 | * - subject - The subject of the e-mail
181 | * - body - The plaintext version of the message
182 | * - html - The HTML version of the message
183 | *
184 | *
185 | * @param {Object} options Message related options
186 | */
187 | MailComposer.prototype.setMessageOption = function(options){
188 | var fields = ["from", "to", "cc", "bcc", "replyTo", "inReplyTo", "references", "subject", "body", "html", "envelope"],
189 | rewrite = {"sender":"from", "reply_to":"replyTo", "text":"body"};
190 |
191 | options = options || {};
192 |
193 | var keys = Object.keys(options), key, value;
194 | for(var i=0, len=keys.length; i= 0){
203 | this._message[key] = this._handleValue(key, value);
204 | }
205 | }
206 | };
207 |
208 | /**
209 | * Setup DKIM for signing generated message. Use with caution as this forces
210 | * the generated message to be cached entirely before emitted.
211 | *
212 | * @param {Object} dkim DKIM signing settings
213 | * @param {String} [dkim.headerFieldNames="from:to:cc:subject"] Header fields to sign
214 | * @param {String} dkim.privateKey DKMI private key
215 | * @param {String} dkim.domainName Domain name to use for signing (ie: "domain.com")
216 | * @param {String} dkim.keySelector Selector for the DKMI public key (ie. "dkim" if you have set up a TXT record for "dkim._domainkey.domain.com"
217 | */
218 | MailComposer.prototype.useDKIM = function(dkim){
219 | this._dkim = dkim || {};
220 | this._cacheOutput = true;
221 | };
222 |
223 |
224 | /**
225 | * Adds an alternative to the list
226 | *
227 | * Following options are allowed:
228 | *
229 | *
230 | * - fileName - filename for the alternative
231 | * - contentType - content type for the attachmetn (default will be derived from the filename)
232 | * - cid - Content ID value for inline images
233 | * - contents - String or Buffer alternative contents
234 | * - filePath - Path to a file for streaming
235 | * - streamSource - Stream object for arbitrary streams
236 | *
237 | *
238 | * One of contents or filePath or stream
239 | * must be specified, otherwise the alternative is not included
240 | *
241 | * @param {Object} alternative Alternative info
242 | */
243 | MailComposer.prototype.addAlternative = function(alternative){
244 | alternative = alternative || {};
245 |
246 | if(!alternative.contentType){
247 | alternative.contentType = "application/octet-stream";
248 | }
249 |
250 | if(!alternative.contentEncoding){
251 | alternative.contentEncoding = "base64";
252 | }
253 |
254 | if(alternative.contents){
255 | this._alternatives.push(alternative);
256 | }
257 | };
258 |
259 |
260 | /**
261 | * Adds an attachment to the list
262 | *
263 | * Following options are allowed:
264 | *
265 | *
266 | * - fileName - filename for the attachment
267 | * - contentType - content type for the attachmetn (default will be derived from the filename)
268 | * - cid - Content ID value for inline images
269 | * - contents - String or Buffer attachment contents
270 | * - filePath - Path to a file for streaming
271 | * - streamSource - Stream object for arbitrary streams
272 | *
273 | *
274 | * One of contents or filePath or stream
275 | * must be specified, otherwise the attachment is not included
276 | *
277 | * @param {Object} attachment Attachment info
278 | */
279 | MailComposer.prototype.addAttachment = function(attachment){
280 | attachment = attachment || {};
281 |
282 | // Needed for Nodemailer compatibility
283 | if(attachment.filename){
284 | attachment.fileName = attachment.filename;
285 | delete attachment.filename;
286 | }
287 |
288 | if(!attachment.fileName && attachment.filePath){
289 | attachment.fileName = attachment.filePath.split(/[\/\\]/).pop();
290 | }
291 |
292 | if(!attachment.contentType){
293 | attachment.contentType = mime.lookup(attachment.fileName || attachment.filePath || "");
294 | }
295 |
296 | if(attachment.streamSource){
297 | // check for pause and resume support
298 | if(typeof attachment.streamSource.pause != "function" ||
299 | typeof attachment.streamSource.resume != "function"){
300 | // Unsupported Stream source, skip it
301 | return;
302 | }
303 | attachment.streamSource.pause();
304 | }
305 |
306 | if(attachment.filePath || attachment.contents || attachment.streamSource){
307 | this._attachments.push(attachment);
308 | }
309 | };
310 |
311 | /**
312 | * Composes and returns an envelope from the this._envelope
313 | * object. Needed for the SMTP client
314 | *
315 | * Generated envelope is int hte following structure:
316 | *
317 | *
318 | * {
319 | * to: "address",
320 | * from: ["list", "of", "addresses"]
321 | * }
322 | *
323 | *
324 | * Both properties (from and to) are optional
325 | * and may not exist
326 | *
327 | * @return {Object} envelope object with "from" and "to" params
328 | */
329 | MailComposer.prototype.getEnvelope = function(){
330 | var envelope = {},
331 | toKeys = ["to", "cc", "bcc"],
332 | key;
333 |
334 | // If multiple addresses, only use the first one
335 | if(this._envelope.from && this._envelope.from.length){
336 | envelope.from = [].concat(this._envelope.from).shift();
337 | }
338 |
339 | for(var i=0, len=toKeys.length; iStarts streaming the message
357 | */
358 | MailComposer.prototype.streamMessage = function(){
359 | if(typeof setImmediate == "function"){
360 | setImmediate(this._composeMessage.bind(this));
361 | }else{
362 | process.nextTick(this._composeMessage.bind(this));
363 | }
364 | };
365 |
366 | /**
367 | * Builds the entire message and returns it as a string
368 | *
369 | * @param {Function} callback Callback function to run
370 | */
371 | MailComposer.prototype.buildMessage = function(callback){
372 | var body = "",
373 | done = false;
374 |
375 | this.on("data", function(chunk){
376 | body += (chunk || "").toString();
377 | });
378 |
379 | this.on("error", function(error){
380 | if(done){
381 | return;
382 | }
383 | done = true;
384 | callback(error);
385 | });
386 |
387 | this.on("end", function(){
388 | if(done){
389 | return;
390 | }
391 | done = true;
392 | callback(null, body);
393 | });
394 |
395 | this.streamMessage();
396 | };
397 |
398 | /**
399 | * Handles backpressure by resuming streaming when possible
400 | */
401 | MailComposer.prototype.resume = function(){
402 | if(this._currentStream && typeof this._currentStream.resume == "function"){
403 | this._currentStream.resume();
404 | }
405 | };
406 |
407 | /**
408 | * Handles backpressure by pausing streaming if required
409 | */
410 | MailComposer.prototype.pause = function(){
411 | if(this._currentStream && typeof this._currentStream.pause == "function"){
412 | this._currentStream.pause();
413 | }
414 | };
415 |
416 | /* PRIVATE API */
417 |
418 | /**
419 | * Handles a message object value, converts addresses etc.
420 | *
421 | * @param {String} key Message options key
422 | * @param {String} value Message options value
423 | * @return {String} converted value
424 | */
425 | MailComposer.prototype._handleValue = function(key, value){
426 | key = (key || "").toString();
427 |
428 | var addresses;
429 |
430 | switch(key){
431 | case "from":
432 | case "to":
433 | case "cc":
434 | case "bcc":
435 | case "replyTo":
436 | value = (value || "").toString().replace(/\r?\n|\r/g, " ");
437 | addresses = [].concat.apply([], [].concat(value).map(mimelib.parseAddresses.bind(mimelib)));
438 | if(!this._envelope.userDefined){
439 | this._envelope[key] = [].concat(this._envelope[key] || []);
440 | addresses.forEach((function(address){
441 | [].concat(address.group || address || []).forEach((function(address){
442 | address = address.address;
443 | if(this._hasUTFChars(address)){
444 | address = toPunycode(address);
445 | }
446 | if(this._envelope[key].indexOf(address) < 0){
447 | this._envelope[key].push(address);
448 | }
449 | }).bind(this));
450 | }).bind(this));
451 | }
452 | return this._convertAddresses(addresses);
453 |
454 | case "inReplyTo":
455 | value = (value || "").toString().replace(/\s/g, "");
456 | if(value.charAt(0)!="<"){
457 | value = "<"+value;
458 | }
459 | if(value.charAt(value.length-1)!=">"){
460 | value = value + ">";
461 | }
462 | return value;
463 |
464 | case "references":
465 | value = [].concat.apply([], [].concat(value || "").map(function(elm){
466 | elm = (elm || "").toString().trim();
467 | return elm.replace(/<[^>]*>/g,function(str){
468 | return str.replace(/\s/g, "");
469 | }).split(/\s+/);
470 | })).map(function(elm){
471 | elm = (elm || "").toString().trim();
472 | if(elm.charAt(0) != "<"){
473 | elm = "<" + elm;
474 | }
475 | if(elm.charAt(elm.length-1) != ">"){
476 | elm = elm + ">";
477 | }
478 | return elm;
479 | });
480 |
481 | return value.join(" ").trim();
482 |
483 | case "subject":
484 | value = (value || "").toString().replace(/\r?\n|\r/g, " ");
485 | return this._encodeMimeWord(value, "Q", 52);
486 |
487 | case "envelope":
488 |
489 | this._envelope = {
490 | userDefined: true
491 | };
492 |
493 | Object.keys(value).forEach((function(key){
494 |
495 | this._envelope[key] = [];
496 |
497 | [].concat(value[key]).forEach((function(address){
498 | var addresses = mimelib.parseAddresses(address);
499 |
500 | this._envelope[key] = this._envelope[key].concat(addresses.map((function(address){
501 | if(this._hasUTFChars(address.address)){
502 | return toPunycode(address.address);
503 | }else{
504 | return address.address;
505 | }
506 | }).bind(this)));
507 |
508 | }).bind(this));
509 | }).bind(this));
510 | break;
511 | }
512 |
513 | return value;
514 | };
515 |
516 | /**
517 | * Handles a list of parsed e-mail addresses, checks encoding etc.
518 | *
519 | * @param {Array} value A list or single e-mail address {address:'...', name:'...'}
520 | * @return {String} Comma separated and encoded list of addresses
521 | */
522 | MailComposer.prototype._convertAddresses = function(addresses){
523 | var values = [], address;
524 |
525 | for(var i=0, len=addresses.length; i';
570 | }
571 | };
572 |
573 | /**
574 | * Gets a header field
575 | *
576 | * @param {String} key Key name
577 | * @return {String|Array} Header field - if several values, then it's an array
578 | */
579 | MailComposer.prototype._getHeader = function(key){
580 | var value;
581 |
582 | key = this._normalizeKey(key);
583 |
584 | value = [].concat(this._headers[key] || []).map(function(val){
585 | return val && val.value || val;
586 | });
587 |
588 | switch(value.length){
589 | case 0: return "";
590 | case 1: return value[0];
591 | default: return value;
592 | }
593 | };
594 |
595 | /**
596 | * Generate an e-mail from the described info
597 | */
598 | MailComposer.prototype._composeMessage = function(){
599 |
600 | // Preprocess functions
601 | if(this.options.forceEmbeddedImages){
602 | this._convertImagesToInline();
603 | }
604 |
605 | // Generate headers for the message
606 | this._composeHeader();
607 |
608 | // Make the mime tree flat
609 | this._flattenMimeTree();
610 |
611 | // Compose message body
612 | this._composeBody();
613 |
614 | };
615 |
616 | /**
617 | * Composes a header for the message and emits it with a 'data'
618 | * event
619 | *
620 | * Also checks and build a structure for the message (is it a multipart message
621 | * and does it need a boundary etc.)
622 | *
623 | * By default the message is not a multipart. If the message containes both
624 | * plaintext and html contents, an alternative block is used. it it containes
625 | * attachments, a mixed block is used. If both alternative and mixed exist, then
626 | * alternative resides inside mixed.
627 | */
628 | MailComposer.prototype._composeHeader = function(){
629 | var headers = [], i, len;
630 |
631 | // if an attachment uses content-id and is linked from the html
632 | // then it should be placed in a separate "related" part with the html
633 | this._message.useRelated = false;
634 | if(this._message.html && (len = this._attachments.length)){
635 |
636 | for(i=len-1; i>=0; i--){
637 | if(this._attachments[i].cid &&
638 | this._message.html.indexOf("cid:"+this._attachments[i].cid)>=0){
639 | this._message.useRelated = true;
640 | this._relatedAttachments.unshift(this._attachments[i]);
641 | this._attachments.splice(i,1);
642 | }
643 | }
644 |
645 | }
646 |
647 | if(this._attachments.length){
648 | this._message.useMixed = true;
649 | this._message.mixedBoundary = this._generateBoundary();
650 | }else{
651 | this._message.useMixed = false;
652 | }
653 |
654 | if([].concat(this._message.body || []).concat(this._message.html || []).
655 | concat(this._alternatives || []).length > 1){
656 | this._message.useAlternative = true;
657 | this._message.alternativeBoundary = this._generateBoundary();
658 | }else{
659 | this._message.useAlternative = false;
660 | }
661 |
662 | // let's do it here, so the counter in the boundary would look better
663 | if(this._message.useRelated){
664 | this._message.relatedBoundary = this._generateBoundary();
665 | }
666 |
667 | if(!this._message.html && !this._message.body){
668 | // If there's nothing to show, show a linebreak
669 | this._message.body = this._lineEnding;
670 | }
671 |
672 | this._buildMessageHeaders();
673 | this._generateBodyStructure();
674 |
675 | // Compile header lines
676 | headers = this.compileHeaders(this._headers);
677 | headers.push("MIME-Version: 1.0");
678 |
679 | if(!this._cacheOutput){
680 | this.emit("data", new Buffer(headers.join(this._lineEnding)+this._lineEnding+this._lineEnding, "utf-8"));
681 | }else{
682 | this._outputBuffer += headers.join(this._lineEnding)+this._lineEnding+this._lineEnding;
683 | }
684 | };
685 |
686 | /**
687 | * Uses data from the this._message object to build headers
688 | */
689 | MailComposer.prototype._buildMessageHeaders = function(){
690 |
691 | // FROM
692 | if(this._message.from && this._message.from.length){
693 | [].concat(this._message.from).forEach((function(from){
694 | this.addHeader("From", from);
695 | }).bind(this));
696 | }
697 |
698 | // TO
699 | if(this._message.to && this._message.to.length){
700 | [].concat(this._message.to).forEach((function(to){
701 | this.addHeader("To", to);
702 | }).bind(this));
703 | }
704 |
705 | // CC
706 | if(this._message.cc && this._message.cc.length){
707 | [].concat(this._message.cc).forEach((function(cc){
708 | this.addHeader("Cc", cc);
709 | }).bind(this));
710 | }
711 |
712 | // BCC
713 | // By default not included, set options.keepBcc to true to keep
714 | if(this.options.keepBcc){
715 | if(this._message.bcc && this._message.bcc.length){
716 | [].concat(this._message.bcc).forEach((function(bcc){
717 | this.addHeader("Bcc", bcc);
718 | }).bind(this));
719 | }
720 | }
721 |
722 | // REPLY-TO
723 | if(this._message.replyTo && this._message.replyTo.length){
724 | [].concat(this._message.replyTo).forEach((function(replyTo){
725 | this.addHeader("Reply-To", replyTo);
726 | }).bind(this));
727 | }
728 |
729 | // If in-reply-to message id is missing from the references, add it automatically
730 | if(this._message.inReplyTo && this._message.inReplyTo.length &&
731 | (this._message.references || "").toString().indexOf(this._message.inReplyTo) < 0){
732 | this._message.references = [].
733 | concat(this._message.inReplyTo).
734 | concat(this._message.references || []).
735 | join(" ");
736 | }
737 |
738 | // REFERENCES
739 | if(this._message.references && this._message.references.length){
740 | this.addHeader("References", this._message.references);
741 | }
742 |
743 | // IN-REPLY-TO
744 | if(this._message.inReplyTo && this._message.inReplyTo.length){
745 | this.addHeader("In-Reply-To", this._message.inReplyTo);
746 | }
747 |
748 | // SUBJECT
749 | if(this._message.subject){
750 | this.addHeader("Subject", this._message.subject);
751 | }
752 | };
753 |
754 | /**
755 | * Generates the structure (mime tree) of the body. This sets up multipart
756 | * structure, individual part headers, boundaries etc.
757 | *
758 | * The headers of the root element will be appended to the message
759 | * headers
760 | */
761 | MailComposer.prototype._generateBodyStructure = function(){
762 |
763 | var tree = this._createMimeNode(),
764 | currentNode, node,
765 | i, len;
766 |
767 | if(this._message.useMixed){
768 |
769 | node = this._createMimeNode();
770 | node.boundary = this._message.mixedBoundary;
771 | node.headers.push(["Content-Type", "multipart/mixed; boundary=\""+node.boundary+"\""]);
772 |
773 | if(currentNode){
774 | currentNode.childNodes.push(node);
775 | node.parentNode = currentNode;
776 | }else{
777 | tree = node;
778 | }
779 | currentNode = node;
780 |
781 | }
782 |
783 | if(this._message.useAlternative){
784 |
785 | node = this._createMimeNode();
786 | node.boundary = this._message.alternativeBoundary;
787 | node.headers.push(["Content-Type", "multipart/alternative; boundary=\""+node.boundary+"\""]);
788 | if(currentNode){
789 | currentNode.childNodes.push(node);
790 | node.parentNode = currentNode;
791 | }else{
792 | tree = node;
793 | }
794 | currentNode = node;
795 |
796 | }
797 |
798 | if(this._message.body){
799 | node = this._createTextComponent(this._message.body, "text/plain");
800 | if(currentNode){
801 | currentNode.childNodes.push(node);
802 | node.parentNode = currentNode;
803 | }else{
804 | tree = node;
805 | }
806 | }
807 |
808 | if(this._message.useRelated){
809 |
810 | node = this._createMimeNode();
811 | node.boundary = this._message.relatedBoundary;
812 | node.headers.push(["Content-Type", "multipart/related; boundary=\""+node.boundary+"\""]);
813 | if(currentNode){
814 | currentNode.childNodes.push(node);
815 | node.parentNode = currentNode;
816 | }else{
817 | tree = node;
818 | }
819 | currentNode = node;
820 |
821 | }
822 |
823 | if(this._message.html){
824 | node = this._createTextComponent(this._message.html, "text/html");
825 | if(currentNode){
826 | currentNode.childNodes.push(node);
827 | node.parentNode = currentNode;
828 | }else{
829 | tree = node;
830 | }
831 | }
832 |
833 | // Alternatives
834 | if(this._alternatives && this._alternatives.length){
835 | for(i=0, len = this._alternatives.length; iCreates a mime tree node for a text component (plaintext, HTML)
875 | *
876 | * @param {String} text Text contents for the component
877 | * @param {String} [contentType="text/plain"] Content type for the text component
878 | * @return {Object} Mime tree node
879 | */
880 | MailComposer.prototype._createTextComponent = function(text, contentType){
881 | var node = this._createMimeNode();
882 |
883 | node.contentEncoding = (this.options.encoding || "quoted-printable").toLowerCase().trim();
884 | node.useTextType = true;
885 |
886 | contentType = [contentType || "text/plain"];
887 | contentType.push("charset=" + this.options.charset);
888 |
889 | if(["7bit", "8bit", "binary"].indexOf(node.contentEncoding)>=0){
890 | node.textFormat = "flowed";
891 | contentType.push("format=" + node.textFormat);
892 | }
893 |
894 | node.headers.push(["Content-Type", contentType.join("; ")]);
895 | node.headers.push(["Content-Transfer-Encoding", node.contentEncoding]);
896 |
897 | node.contents = text;
898 |
899 | return node;
900 | };
901 |
902 | /**
903 | * Creates a mime tree node for an attachment component
904 | *
905 | * @param {Object} attachment Attachment info for the component
906 | * @return {Object} Mime tree node
907 | */
908 | MailComposer.prototype._createAttachmentComponent = function(attachment){
909 | var node = this._createMimeNode(),
910 | contentType = [attachment.contentType],
911 | contentDisposition = [attachment.contentDisposition || "attachment"],
912 | fileName;
913 |
914 | node.contentEncoding = "base64";
915 |
916 | if(attachment.fileName){
917 | fileName = this._encodeMimeWord(attachment.fileName, "Q", 1024).replace(/"/g,"\\\"");
918 | contentType.push("name=\"" +fileName+ "\"");
919 | contentDisposition.push("filename=\"" +fileName+ "\"");
920 | }
921 |
922 | node.headers.push(["Content-Type", contentType.join("; ")]);
923 | node.headers.push(["Content-Disposition", contentDisposition.join("; ")]);
924 | node.headers.push(["Content-Transfer-Encoding", node.contentEncoding]);
925 |
926 | if(attachment.cid){
927 | node.headers.push(["Content-Id", "<" + this._encodeMimeWord(attachment.cid) + ">"]);
928 | }
929 |
930 | if(attachment.contents){
931 | node.contents = attachment.contents;
932 | }else if(attachment.filePath){
933 | node.filePath = attachment.filePath;
934 | if(attachment.userAgent){
935 | node.userAgent = attachment.userAgent;
936 | }
937 | }else if(attachment.streamSource){
938 | node.streamSource = attachment.streamSource;
939 | }
940 |
941 | return node;
942 | };
943 |
944 | /**
945 | * Creates a mime tree node for an alternative text component (ODF/DOC/etc)
946 | *
947 | * @param {Object} alternative Alternative info for the component
948 | * @return {Object} Mime tree node
949 | */
950 | MailComposer.prototype._createAlternativeComponent = function(alternative){
951 | var node = this._createMimeNode(),
952 | contentType = alternative.contentType.split(";").map(function(part){
953 | return (part || "").trim();
954 | });
955 |
956 | node.contentEncoding = alternative.contentEncoding || "base64";
957 |
958 | if(["7bit", "8bit", "binary"].indexOf(node.contentEncoding)>=0){
959 | node.textFormat = "flowed";
960 | contentType.push("format=" + node.textFormat);
961 | }
962 |
963 | node.headers.push(["Content-Type", contentType.join("; ")]);
964 | node.headers.push(["Content-Transfer-Encoding", node.contentEncoding]);
965 |
966 | node.contents = alternative.contents;
967 |
968 | return node;
969 | };
970 |
971 | /**
972 | * Creates an empty mime tree node
973 | *
974 | * @return {Object} Mime tree node
975 | */
976 | MailComposer.prototype._createMimeNode = function(){
977 | return {
978 | childNodes: [],
979 | headers: [],
980 | parentNode: null
981 | };
982 | };
983 |
984 | /**
985 | * Compiles headers object into an array of header lines. If needed, the
986 | * lines are folded
987 | *
988 | * @param {Object|Array} headers An object with headers in the form of
989 | * {key:value} or [[key, value]] or
990 | * [{key:key, value: value}]
991 | * @return {Array} A list of header lines. Can be joined with \r\n
992 | */
993 | MailComposer.prototype.compileHeaders = function(headers){
994 | var headersArr = [], keys, key;
995 |
996 | if(Array.isArray(headers)){
997 | headersArr = headers.map((function(field){
998 | var key = this._normalizeKey(field.key || field[0]),
999 | value = field.value || field[1];
1000 |
1001 | if(typeof value == "string"){
1002 | value = {
1003 | formatted: false,
1004 | value: value
1005 | };
1006 | }
1007 |
1008 | if(value.formatted){
1009 | return key + ": " +value.value;
1010 | }
1011 |
1012 | return mimelib.foldLine(key + ": " + this._sanitizeHeaderValue(value.value), 76, false, false, 52);
1013 | }).bind(this));
1014 | }else{
1015 | keys = Object.keys(headers);
1016 | for(var i=0, len = keys.length; iConverts a structured mimetree into an one dimensional array of
1054 | * components. This includes headers and multipart boundaries as strings,
1055 | * textual and attachment contents are.
1056 | */
1057 | MailComposer.prototype._flattenMimeTree = function(){
1058 | var flatTree = [];
1059 |
1060 | var walkTree = (function(node, level){
1061 | var contentObject = {};
1062 | level = level || 0;
1063 |
1064 | // if not root element, include headers
1065 | if(level){
1066 | flatTree = flatTree.concat(this.compileHeaders(node.headers));
1067 | flatTree.push('');
1068 | }
1069 |
1070 | if(node.textFormat){
1071 | contentObject.textFormat = node.textFormat;
1072 | }
1073 |
1074 | if(node.contentEncoding){
1075 | contentObject.contentEncoding = node.contentEncoding;
1076 | }
1077 |
1078 | if(node.contents){
1079 | contentObject.contents = node.contents;
1080 | }else if(node.filePath){
1081 | contentObject.filePath = node.filePath;
1082 | if(node.userAgent){
1083 | contentObject.userAgent = node.userAgent;
1084 | }
1085 | }else if(node.streamSource){
1086 | contentObject.streamSource = node.streamSource;
1087 | }
1088 |
1089 | if(node.contents || node.filePath || node.streamSource){
1090 | flatTree.push(contentObject);
1091 | }
1092 |
1093 | // walk children
1094 | for(var i=0, len = node.childNodes.length; iComposes the e-mail body based on the previously generated mime tree
1117 | *
1118 | * Assumes that the linebreak separating headers and contents is already
1119 | * sent
1120 | *
1121 | * Emits 'data' events
1122 | */
1123 | MailComposer.prototype._composeBody = function(){
1124 | var flatTree = this._message.flatTree,
1125 | slice, isObject = false, isEnd = false,
1126 | curObject;
1127 |
1128 | this._message.processingStart = this._message.processingStart || 0;
1129 | this._message.processingPos = this._message.processingPos || 0;
1130 |
1131 | for(var len = flatTree.length; this._message.processingPos < len; this._message.processingPos++){
1132 |
1133 | isEnd = this._message.processingPos >= len-1;
1134 | isObject = typeof flatTree[this._message.processingPos] == "object";
1135 |
1136 | if(isEnd || isObject){
1137 |
1138 | slice = flatTree.slice(this._message.processingStart, isEnd && !isObject?undefined:this._message.processingPos);
1139 | if(slice && slice.length){
1140 | if(!this._cacheOutput){
1141 | this.emit("data", new Buffer(slice.join(this._lineEnding)+this._lineEnding, "utf-8"));
1142 | }else{
1143 | this._outputBuffer += slice.join(this._lineEnding)+this._lineEnding;
1144 | }
1145 | }
1146 |
1147 | if(isObject){
1148 | curObject = flatTree[this._message.processingPos];
1149 |
1150 | this._message.processingPos++;
1151 | this._message.processingStart = this._message.processingPos;
1152 |
1153 | this._emitDataElement(curObject, (function(){
1154 | if(!isEnd){
1155 | if(typeof setImmediate == "function"){
1156 | setImmediate(this._composeBody.bind(this));
1157 | }else{
1158 | process.nextTick(this._composeBody.bind(this));
1159 | }
1160 | }else{
1161 | if(!this._cacheOutput){
1162 | this.emit("end");
1163 | }else{
1164 | this._processBufferedOutput();
1165 | }
1166 | }
1167 | }).bind(this));
1168 |
1169 | }else if(isEnd){
1170 | if(!this._cacheOutput){
1171 | this.emit("end");
1172 | }else{
1173 | this._processBufferedOutput();
1174 | }
1175 | }
1176 | break;
1177 | }
1178 |
1179 | }
1180 | };
1181 |
1182 | /**
1183 | * Emits a data event for a text or html body and attachments. If it is a
1184 | * file, stream it
1185 | *
1186 | * If this.options.escapeSMTP is true, replace dots in the
1187 | * beginning of a line with double dots - only valid for QP encoding
1188 | *
1189 | * @param {Object} element Data element descriptor
1190 | * @param {Function} callback Callback function to run when completed
1191 | */
1192 | MailComposer.prototype._emitDataElement = function(element, callback){
1193 |
1194 | var data = "";
1195 |
1196 | if(element.contents){
1197 | switch(element.contentEncoding){
1198 | case "quoted-printable":
1199 | data = mimelib.encodeQuotedPrintable(element.contents, false, this.options.charset);
1200 | break;
1201 | case "base64":
1202 | data = new Buffer(element.contents, "utf-8").toString("base64").replace(/.{76}/g,"$&\r\n");
1203 | break;
1204 | //case "7bit":
1205 | //case "8bit":
1206 | //case "binary":
1207 | default:
1208 | data = mimelib.foldLine(element.contents, 76, false, element.textFormat=="flowed");
1209 | //mimelib puts a long whitespace to the beginning of the lines
1210 | data = data.replace(/^[ ]{7}/mg, "");
1211 | break;
1212 | }
1213 |
1214 | if(this.options.escapeSMTP){
1215 | data = data.replace(/^\./gm,'..');
1216 | }
1217 |
1218 | if(!this._cacheOutput){
1219 | this.emit("data", new Buffer(data + this._lineEnding, "utf-8"));
1220 | }else{
1221 | this._outputBuffer += data + this._lineEnding;
1222 | }
1223 |
1224 | if(typeof setImmediate == "function"){
1225 | setImmediate(callback);
1226 | }else{
1227 | process.nextTick(callback);
1228 | }
1229 | return;
1230 | }
1231 |
1232 | if(element.filePath){
1233 | if(element.filePath.match(/^https?:\/\//)){
1234 | this._serveStream(urlFetch(element.filePath, {userAgent: element.userAgent}), callback);
1235 | }else{
1236 | this._serveFile(element.filePath, callback);
1237 | }
1238 | return;
1239 | }else if(element.streamSource){
1240 | this._serveStream(element.streamSource, callback);
1241 | return;
1242 | }
1243 |
1244 | callback();
1245 | };
1246 |
1247 | /**
1248 | * Pipes a file to the e-mail stream
1249 | *
1250 | * @param {String} filePath Path to the file
1251 | * @param {Function} callback Callback function to run after completion
1252 | */
1253 | MailComposer.prototype._serveFile = function(filePath, callback){
1254 | fs.stat(filePath, (function(err, stat){
1255 | if(err || !stat.isFile()){
1256 |
1257 |
1258 | if(!this._cacheOutput){
1259 | this.emit("data", new Buffer(new Buffer("",
1260 | "utf-8").toString("base64")+this._lineEnding, "utf-8"));
1261 | }else{
1262 | this._outputBuffer += new Buffer("",
1263 | "utf-8").toString("base64")+this._lineEnding;
1264 | }
1265 |
1266 | if(typeof setImmediate == "function"){
1267 | setImmediate(callback);
1268 | }else{
1269 | process.nextTick(callback);
1270 | }
1271 | return;
1272 | }
1273 |
1274 | var stream = fs.createReadStream(filePath);
1275 |
1276 | this._serveStream(stream, callback);
1277 |
1278 | }).bind(this));
1279 | };
1280 |
1281 | /**
1282 | * Pipes a stream source to the e-mail stream
1283 | *
1284 | * This function resumes the stream and starts sending 76 bytes long base64
1285 | * encoded lines. To achieve this, the incoming stream is divded into
1286 | * chunks of 57 bytes (57/3*4=76) to achieve exactly 76 byte long
1287 | * base64
1288 | *
1289 | * @param {Object} stream Stream to be piped
1290 | * @param {Function} callback Callback function to run after completion
1291 | */
1292 | MailComposer.prototype._serveStream = function(stream, callback){
1293 | var remainder = new Buffer(0);
1294 |
1295 | this._currentStream = stream;
1296 |
1297 | stream.on("error", (function(error){
1298 | if(!this._cacheOutput){
1299 | this.emit("data", new Buffer(new Buffer("\n" + error.message,
1300 | "utf-8").toString("base64")+this._lineEnding, "utf-8"));
1301 | }else{
1302 | this._outputBuffer += new Buffer("\n" + error.message,
1303 | "utf-8").toString("base64")+this._lineEnding;
1304 | }
1305 |
1306 | this._currentStream = false;
1307 |
1308 | if(typeof setImmediate == "function"){
1309 | setImmediate(callback);
1310 | }else{
1311 | process.nextTick(callback);
1312 | }
1313 | }).bind(this));
1314 |
1315 | stream.on("data", (function(chunk){
1316 | var data = "",
1317 | len = remainder.length + chunk.length,
1318 | remainderLength = len % 57, // we use 57 bytes as it composes
1319 | // a 76 bytes long base64 string
1320 | buffer = new Buffer(len);
1321 |
1322 | remainder.copy(buffer); // copy remainder into the beginning of the new buffer
1323 | chunk.copy(buffer, remainder.length); // copy data chunk after the remainder
1324 | remainder = buffer.slice(len - remainderLength); // create a new remainder
1325 |
1326 | data = buffer.slice(0, len - remainderLength).toString("base64").replace(/.{76}/g,"$&\r\n");
1327 |
1328 | if(data.length){
1329 | if(!this._cacheOutput){
1330 | this.emit("data", new Buffer(data.trim()+this._lineEnding, "utf-8"));
1331 | }else{
1332 | this._outputBuffer += data.trim()+this._lineEnding;
1333 | }
1334 | }
1335 | }).bind(this));
1336 |
1337 | stream.on("end", (function(){
1338 | var data;
1339 |
1340 | // stream the remainder (if any)
1341 | if(remainder.length){
1342 | data = remainder.toString("base64").replace(/.{76}/g,"$&\r\n");
1343 | if(!this._cacheOutput){
1344 | this.emit("data", new Buffer(data.trim() + this._lineEnding, "utf-8"));
1345 | }else{
1346 | this._outputBuffer += data.trim() + this._lineEnding;
1347 | }
1348 | }
1349 |
1350 | this._currentStream = false;
1351 |
1352 | if(typeof setImmediate == "function"){
1353 | setImmediate(callback);
1354 | }else{
1355 | process.nextTick(callback);
1356 | }
1357 | }).bind(this));
1358 |
1359 | // resume streaming if paused
1360 | stream.resume();
1361 | };
1362 |
1363 | /**
1364 | * Processes buffered output and emits 'end'
1365 | */
1366 | MailComposer.prototype._processBufferedOutput = function(){
1367 | var dkimSignature;
1368 |
1369 | if(this._dkim){
1370 | if((dkimSignature = runDKIMSign(this.options.escapeSMTP ? this._outputBuffer.replace(/^\.\./mg, ".") : this._outputBuffer, this._dkim))){
1371 | this.emit("data", new Buffer(dkimSignature+this._lineEnding, "utf-8"));
1372 | }
1373 | }
1374 |
1375 | this.emit("data", new Buffer(this._outputBuffer, "utf-8"));
1376 |
1377 | if(typeof setImmediate == "function"){
1378 | setImmediate(this.emit.bind(this,"end"));
1379 | }else{
1380 | process.nextTick(this.emit.bind(this,"end"));
1381 | }
1382 | };
1383 |
1384 | /* HELPER FUNCTIONS */
1385 |
1386 | /**
1387 | * Normalizes a key name by cpitalizing first chars of words, except for
1388 | * custom keys (starting with "X-") that have only uppercase letters, which will
1389 | * not be modified.
1390 | *
1391 | * x-mailer will become X-Mailer
1392 | *
1393 | * Needed to avoid duplicate header keys
1394 | *
1395 | * @param {String} key Key name
1396 | * @return {String} First chars uppercased
1397 | */
1398 | MailComposer.prototype._normalizeKey = function(key){
1399 | key = (key || "").toString().trim();
1400 |
1401 | // If only uppercase letters, leave everything as is
1402 | if(key.match(/^X\-[A-Z0-9\-]+$/)){
1403 | return key;
1404 | }
1405 |
1406 | // Convert first letter upper case, others lower case
1407 | return key.
1408 | toLowerCase().
1409 | replace(/^\S|[\-\s]\S/g, function(c){
1410 | return c.toUpperCase();
1411 | }).
1412 | replace(/^MIME\-/i, "MIME-").
1413 | replace(/^DKIM\-/i, "DKIM-");
1414 | };
1415 |
1416 | /**
1417 | * Tests if a string has high bit (UTF-8) symbols
1418 | *
1419 | * @param {String} str String to be tested for high bit symbols
1420 | * @return {Boolean} true if high bit symbols were found
1421 | */
1422 | MailComposer.prototype._hasUTFChars = function(str){
1423 | var rforeign = /[^\u0000-\u007f]/;
1424 | return !!rforeign.test(str);
1425 | };
1426 |
1427 | /**
1428 | * Generates a boundary for multipart bodies
1429 | *
1430 | * @return {String} Boundary String
1431 | */
1432 | MailComposer.prototype._generateBoundary = function(){
1433 | // "_" is not allowed in quoted-printable and "?" not in base64
1434 | return "----" + this.options.identityString + "-?=_"+(++this._gencounter)+"-"+Date.now();
1435 | };
1436 |
1437 | /**
1438 | * Converts a string to mime word format. If the length is longer than
1439 | * maxlen, split it
1440 | *
1441 | * If the string doesn't have any unicode characters return the original
1442 | * string instead
1443 | *
1444 | * @param {String} str String to be encoded
1445 | * @param {String} encoding Either Q for Quoted-Printable or B for Base64
1446 | * @param {Number} [maxlen] Optional length of the resulting string, whitespace will be inserted if needed
1447 | *
1448 | * @return {String} Mime-word encoded string (if needed)
1449 | */
1450 | MailComposer.prototype._encodeMimeWord = function(str, encoding, maxlen){
1451 | return mimelib.encodeMimeWords(str, encoding, maxlen, this.options.charset);
1452 | };
1453 |
1454 | /**
1455 | * Splits a mime-encoded string
1456 | *
1457 | * @param {String} str Input string
1458 | * @param {Number} maxlen Maximum line length
1459 | * @return {Array} split string
1460 | */
1461 | MailComposer.prototype._splitEncodedString = function(str, maxlen){
1462 | var curLine, match, chr, done,
1463 | lines = [];
1464 |
1465 | while(str.length){
1466 | curLine = str.substr(0, maxlen);
1467 |
1468 | // move incomplete escaped char back to main
1469 | if((match = curLine.match(/\=[0-9A-F]?$/i))){
1470 | curLine = curLine.substr(0, match.index);
1471 | }
1472 |
1473 | done = false;
1474 | while(!done){
1475 | done = true;
1476 | // check if not middle of a unicode char sequence
1477 | if((match = str.substr(curLine.length).match(/^\=([0-9A-F]{2})/i))){
1478 | chr = parseInt(match[1], 16);
1479 | // invalid sequence, move one char back anc recheck
1480 | if(chr < 0xC2 && chr > 0x7F){
1481 | curLine = curLine.substr(0, curLine.length-3);
1482 | done = false;
1483 | }
1484 | }
1485 | }
1486 |
1487 | if(curLine.length){
1488 | lines.push(curLine);
1489 | }
1490 | str = str.substr(curLine.length);
1491 | }
1492 |
1493 | return lines;
1494 | };
1495 |
1496 | /**
1497 | * Detects image urls and paths from HTML code and replaces with attachments
1498 | * for embedding images inline
1499 | */
1500 | MailComposer.prototype._convertImagesToInline = function(){
1501 | if(!this._message.html){
1502 | return;
1503 | }
1504 |
1505 | this._message.html = this._message.html.replace(/
]*>/gi, (function(imgTag){
1506 | return imgTag.replace(/\b(src\s*=\s*(?:['"]?))([^'"> ]+)/i, (function(src, prefix, url){
1507 | var cid;
1508 | url = he.decode(url || "").trim();
1509 | prefix = prefix || "";
1510 |
1511 | if(url.match(/^https?:\/\//i) || url.match(/^\//i) || url.match(/^[a-z]:\\/i)){
1512 | cid = crypto.randomBytes(20).toString("hex") + "@" + this.options.identityString;
1513 | this.addAttachment({
1514 | filePath: url,
1515 | cid: cid
1516 | });
1517 | url = "cid:"+cid;
1518 | }
1519 |
1520 | return prefix + url;
1521 | }).bind(this));
1522 | }).bind(this));
1523 | };
1524 |
--------------------------------------------------------------------------------
/test/textfile.txt:
--------------------------------------------------------------------------------
1 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
2 |
3 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
4 |
5 | "Welding an old pike-head, sir; there were seams and dents in it."
6 |
7 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
8 |
9 | "I think so, sir."
10 |
11 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
12 |
13 | "Aye, sir, I think I can; all seams and dents but one."
14 |
15 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
16 |
17 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
18 |
19 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
20 |
21 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
22 |
23 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
24 |
25 | "Welding an old pike-head, sir; there were seams and dents in it."
26 |
27 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
28 |
29 | "I think so, sir."
30 |
31 | Some unicode symbols ÕÄÖÜ ↑, →, ↓, ↔, ↕, ↖, ↗, ↘, ↙, ↚, ↛, ↜, ↝, ↞, ↟, ↠, ↡, ↢, ↣, ↤, ↥, ↦
32 |
33 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
34 |
35 | "Aye, sir, I think I can; all seams and dents but one."
36 |
37 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
38 |
39 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
40 |
41 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
42 |
43 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
44 |
45 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
46 |
47 | "Welding an old pike-head, sir; there were seams and dents in it."
48 |
49 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
50 |
51 | "I think so, sir."
52 |
53 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
54 |
55 | "Aye, sir, I think I can; all seams and dents but one."
56 |
57 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
58 |
59 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
60 |
61 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
62 |
63 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
64 |
65 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
66 |
67 | "Welding an old pike-head, sir; there were seams and dents in it."
68 |
69 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
70 |
71 | "I think so, sir."
72 |
73 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
74 |
75 | "Aye, sir, I think I can; all seams and dents but one."
76 |
77 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
78 |
79 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
80 |
81 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
82 |
83 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
84 |
85 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
86 |
87 | "Welding an old pike-head, sir; there were seams and dents in it."
88 |
89 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
90 |
91 | "I think so, sir."
92 |
93 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
94 |
95 | "Aye, sir, I think I can; all seams and dents but one."
96 |
97 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
98 |
99 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
100 |
101 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
102 |
103 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
104 |
105 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
106 |
107 | "Welding an old pike-head, sir; there were seams and dents in it."
108 |
109 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
110 |
111 | "I think so, sir."
112 |
113 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
114 |
115 | "Aye, sir, I think I can; all seams and dents but one."
116 |
117 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
118 |
119 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
120 |
121 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
122 |
123 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
124 |
125 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
126 |
127 | "Welding an old pike-head, sir; there were seams and dents in it."
128 |
129 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
130 |
131 | "I think so, sir."
132 |
133 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
134 |
135 | "Aye, sir, I think I can; all seams and dents but one."
136 |
137 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
138 |
139 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
140 |
141 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
142 |
143 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
144 |
145 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
146 |
147 | "Welding an old pike-head, sir; there were seams and dents in it."
148 |
149 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
150 |
151 | "I think so, sir."
152 |
153 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
154 |
155 | "Aye, sir, I think I can; all seams and dents but one."
156 |
157 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
158 |
159 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
160 |
161 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
162 |
163 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
164 |
165 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
166 |
167 | "Welding an old pike-head, sir; there were seams and dents in it."
168 |
169 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
170 |
171 | "I think so, sir."
172 |
173 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
174 |
175 | "Aye, sir, I think I can; all seams and dents but one."
176 |
177 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
178 |
179 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
180 |
181 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
182 |
183 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
184 |
185 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
186 |
187 | "Welding an old pike-head, sir; there were seams and dents in it."
188 |
189 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
190 |
191 | "I think so, sir."
192 |
193 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
194 |
195 | "Aye, sir, I think I can; all seams and dents but one."
196 |
197 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
198 |
199 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
200 |
201 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
202 |
203 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
204 |
205 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
206 |
207 | "Welding an old pike-head, sir; there were seams and dents in it."
208 |
209 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
210 |
211 | "I think so, sir."
212 |
213 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
214 |
215 | "Aye, sir, I think I can; all seams and dents but one."
216 |
217 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
218 |
219 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
220 |
221 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
222 |
223 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
224 |
225 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
226 |
227 | "Welding an old pike-head, sir; there were seams and dents in it."
228 |
229 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
230 |
231 | "I think so, sir."
232 |
233 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
234 |
235 | "Aye, sir, I think I can; all seams and dents but one."
236 |
237 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
238 |
239 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
240 |
241 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
242 |
243 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
244 |
245 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
246 |
247 | "Welding an old pike-head, sir; there were seams and dents in it."
248 |
249 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
250 |
251 | "I think so, sir."
252 |
253 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
254 |
255 | "Aye, sir, I think I can; all seams and dents but one."
256 |
257 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
258 |
259 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
260 |
261 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
262 |
263 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
264 |
265 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
266 |
267 | "Welding an old pike-head, sir; there were seams and dents in it."
268 |
269 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
270 |
271 | "I think so, sir."
272 |
273 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
274 |
275 | "Aye, sir, I think I can; all seams and dents but one."
276 |
277 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
278 |
279 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
280 |
281 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
282 |
283 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
284 |
285 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
286 |
287 | "Welding an old pike-head, sir; there were seams and dents in it."
288 |
289 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
290 |
291 | "I think so, sir."
292 |
293 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
294 |
295 | "Aye, sir, I think I can; all seams and dents but one."
296 |
297 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
298 |
299 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
300 |
301 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
302 |
303 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
304 |
305 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
306 |
307 | "Welding an old pike-head, sir; there were seams and dents in it."
308 |
309 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
310 |
311 | "I think so, sir."
312 |
313 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
314 |
315 | "Aye, sir, I think I can; all seams and dents but one."
316 |
317 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
318 |
319 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
320 |
321 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
322 |
323 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
324 |
325 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
326 |
327 | "Welding an old pike-head, sir; there were seams and dents in it."
328 |
329 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
330 |
331 | "I think so, sir."
332 |
333 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
334 |
335 | "Aye, sir, I think I can; all seams and dents but one."
336 |
337 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
338 |
339 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
340 |
341 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
342 |
343 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
344 |
345 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
346 |
347 | "Welding an old pike-head, sir; there were seams and dents in it."
348 |
349 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
350 |
351 | "I think so, sir."
352 |
353 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
354 |
355 | "Aye, sir, I think I can; all seams and dents but one."
356 |
357 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
358 |
359 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
360 |
361 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
362 |
363 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
364 |
365 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
366 |
367 | "Welding an old pike-head, sir; there were seams and dents in it."
368 |
369 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
370 |
371 | "I think so, sir."
372 |
373 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
374 |
375 | "Aye, sir, I think I can; all seams and dents but one."
376 |
377 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
378 |
379 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
380 |
381 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
382 |
383 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
384 |
385 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
386 |
387 | "Welding an old pike-head, sir; there were seams and dents in it."
388 |
389 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
390 |
391 | "I think so, sir."
392 |
393 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
394 |
395 | "Aye, sir, I think I can; all seams and dents but one."
396 |
397 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
398 |
399 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
400 |
401 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
402 |
403 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
404 |
405 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
406 |
407 | "Welding an old pike-head, sir; there were seams and dents in it."
408 |
409 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
410 |
411 | "I think so, sir."
412 |
413 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
414 |
415 | "Aye, sir, I think I can; all seams and dents but one."
416 |
417 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
418 |
419 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
420 |
421 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
422 |
423 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
424 |
425 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
426 |
427 | "Welding an old pike-head, sir; there were seams and dents in it."
428 |
429 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
430 |
431 | "I think so, sir."
432 |
433 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
434 |
435 | "Aye, sir, I think I can; all seams and dents but one."
436 |
437 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
438 |
439 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
440 |
441 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
442 |
443 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
444 |
445 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
446 |
447 | "Welding an old pike-head, sir; there were seams and dents in it."
448 |
449 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
450 |
451 | "I think so, sir."
452 |
453 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
454 |
455 | "Aye, sir, I think I can; all seams and dents but one."
456 |
457 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
458 |
459 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
460 |
461 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
462 |
463 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
464 |
465 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
466 |
467 | "Welding an old pike-head, sir; there were seams and dents in it."
468 |
469 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
470 |
471 | "I think so, sir."
472 |
473 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
474 |
475 | "Aye, sir, I think I can; all seams and dents but one."
476 |
477 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
478 |
479 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
480 |
481 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
482 |
483 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
484 |
485 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
486 |
487 | "Welding an old pike-head, sir; there were seams and dents in it."
488 |
489 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
490 |
491 | "I think so, sir."
492 |
493 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
494 |
495 | "Aye, sir, I think I can; all seams and dents but one."
496 |
497 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
498 |
499 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
500 |
501 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
502 |
503 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
504 |
505 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
506 |
507 | "Welding an old pike-head, sir; there were seams and dents in it."
508 |
509 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
510 |
511 | "I think so, sir."
512 |
513 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
514 |
515 | "Aye, sir, I think I can; all seams and dents but one."
516 |
517 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
518 |
519 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
520 |
521 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
522 |
523 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
524 |
525 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
526 |
527 | "Welding an old pike-head, sir; there were seams and dents in it."
528 |
529 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
530 |
531 | "I think so, sir."
532 |
533 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
534 |
535 | "Aye, sir, I think I can; all seams and dents but one."
536 |
537 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
538 |
539 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
540 |
541 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
542 |
543 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
544 |
545 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
546 |
547 | "Welding an old pike-head, sir; there were seams and dents in it."
548 |
549 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
550 |
551 | "I think so, sir."
552 |
553 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
554 |
555 | "Aye, sir, I think I can; all seams and dents but one."
556 |
557 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
558 |
559 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
560 |
561 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
562 |
563 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
564 |
565 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
566 |
567 | "Welding an old pike-head, sir; there were seams and dents in it."
568 |
569 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
570 |
571 | "I think so, sir."
572 |
573 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
574 |
575 | "Aye, sir, I think I can; all seams and dents but one."
576 |
577 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
578 |
579 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
580 |
581 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
582 |
583 | "Because I am scorched all over, Captain Ahab," answered Perth, resting for a moment on his hammer; "I am past scorching; not easily can'st thou scorch a scar."
584 |
585 | "Well, well; no more. Thy shrunk voice sounds too calmly, sanely woeful to me. In no Paradise myself, I am impatient of all misery in others that is not mad. Thou should'st go mad, blacksmith; say, why dost thou not go mad? How can'st thou endure without being mad? Do the heavens yet hate thee, that thou can'st not go mad?—What wert thou making there?"
586 |
587 | "Welding an old pike-head, sir; there were seams and dents in it."
588 |
589 | "And can'st thou make it all smooth again, blacksmith, after such hard usage as it had?"
590 |
591 | "I think so, sir."
592 |
593 | "And I suppose thou can'st smoothe almost any seams and dents; never mind how hard the metal, blacksmith?"
594 |
595 | "Aye, sir, I think I can; all seams and dents but one."
596 |
597 | "Look ye here, then," cried Ahab, passionately advancing, and leaning with both hands on Perth's shoulders; "look ye here—HERE—can ye smoothe out a seam like this, blacksmith," sweeping one hand across his ribbed brow; "if thou could'st, blacksmith, glad enough would I lay my head upon thy anvil, and feel thy heaviest hammer between my eyes. Answer! Can'st thou smoothe this seam?"
598 |
599 | "Oh! that is the one, sir! Said I not all seams and dents but one?"
600 |
601 | "Aye, blacksmith, it is the one; aye, man, it is unsmoothable; for though thou only see'st it here in my flesh, it has worked down into the bone of my skull—THAT is all wrinkles! But, away with child's play; no more gaffs and pikes to-day. Look ye here!" jingling the leathern bag, as if it were full of gold coins. "I, too, want a harpoon made; one that a thousand yoke of fiends could not part, Perth; something that will stick in a whale like his own fin-bone. There's the stuff," flinging the pouch upon the anvil. "Look ye, blacksmith, these are the gathered nail-stubbs of the steel shoes of racing horses."
602 |
--------------------------------------------------------------------------------