├── .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 | [![Build Status](https://secure.travis-ci.org/andris9/mailcomposer.png)](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 | [![Donate to author](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](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 | --------------------------------------------------------------------------------