all repos — mail2 @ 1743ddd5b0a0e18cd934fc3385f910ba2d97b88b

fork of github.com/joegrasse/mail with some changes

mail.go (view raw)

  1package mail
  2
  3import (
  4	"bytes"
  5	"crypto/tls"
  6	"encoding/base64"
  7	"errors"
  8	"fmt"
  9	"github.com/joegrasse/mime/header"
 10	"io"
 11	"io/ioutil"
 12	"mime"
 13	"mime/multipart"
 14	"mime/quotedprintable"
 15	"net"
 16	"net/mail"
 17	"net/smtp"
 18	"net/textproto"
 19	"path/filepath"
 20	"regexp"
 21	"strconv"
 22	"strings"
 23	"time"
 24)
 25
 26// email represents an email message.
 27type email struct {
 28	from           string
 29	sender         string
 30	replyTo        string
 31	returnPath     string
 32	recipients     []string
 33	headers        textproto.MIMEHeader
 34	parts          []part
 35	attachments    []*file
 36	inlines        []*file
 37	Charset        string
 38	Encoding       encoding
 39	Encryption     encryption
 40	Username       string
 41	Password       string
 42	TLSConfig      *tls.Config
 43	ConnectTimeout int
 44}
 45
 46// part represents the different content parts of an email body.
 47type part struct {
 48	contentType string
 49	body        *bytes.Buffer
 50}
 51
 52// file represents the files that can be added to the email message.
 53type file struct {
 54	filename string
 55	mimeType string
 56	data     []byte
 57}
 58
 59type encryption int
 60
 61const (
 62	EncryptionTLS encryption = iota
 63	EncryptionSSL
 64	EncryptionNone
 65)
 66
 67var encryptionTypes = [...]string{"TLS", "SSL", "None"}
 68
 69func (encryption encryption) String() string {
 70	return encryptionTypes[encryption]
 71}
 72
 73type encoding int
 74
 75const (
 76	EncodingQuotedPrintable encoding = iota
 77	EncodingBase64
 78	EncodingNone
 79)
 80
 81var encodingTypes = [...]string{"quoted-printable", "base64", "binary"}
 82
 83func (encoding encoding) String() string {
 84	return encodingTypes[encoding]
 85}
 86
 87// New creates a new email. It uses UTF-8 by default.
 88func New() *email {
 89	email := &email{
 90		headers:    make(textproto.MIMEHeader),
 91		Charset:    "UTF-8",
 92		Encoding:   EncodingQuotedPrintable,
 93		Encryption: EncryptionNone,
 94		TLSConfig:  new(tls.Config),
 95	}
 96
 97	email.AddHeader("MIME-Version", "1.0")
 98
 99	return email
100}
101
102// SetFrom sets the From address.
103func (email *email) SetFrom(address string) error {
104	return email.AddAddresses("From", address)
105}
106
107// SetSender sets the Sender address.
108func (email *email) SetSender(address string) error {
109	return email.AddAddresses("Sender", address)
110}
111
112// SetReplyTo sets the Reply-To address.
113func (email *email) SetReplyTo(address string) error {
114	return email.AddAddresses("Reply-To", address)
115}
116
117// SetReturnPath sets the Return-Path address. This is most often used
118// to send bounced emails to a different email address.
119func (email *email) SetReturnPath(address string) error {
120	return email.AddAddresses("Return-Path", address)
121}
122
123// AddTo adds a To address. You can provide multiple
124// addresses at the same time.
125func (email *email) AddTo(addresses ...string) error {
126	return email.AddAddresses("To", addresses...)
127}
128
129// AddCc adds a Cc address. You can provide multiple
130// addresses at the same time.
131func (email *email) AddCc(addresses ...string) error {
132	return email.AddAddresses("Cc", addresses...)
133}
134
135// AddBcc adds a Bcc address. You can provide multiple
136// addresses at the same time.
137func (email *email) AddBcc(addresses ...string) error {
138	return email.AddAddresses("Bcc", addresses...)
139}
140
141// AddAddresses allows you to add addresses to the specified address header.
142func (email *email) AddAddresses(header string, addresses ...string) error {
143	found := false
144
145	// check for a valid address header
146	for _, h := range []string{"To", "Cc", "Bcc", "From", "Sender", "Reply-To", "Return-Path"} {
147		if header == h {
148			found = true
149		}
150	}
151
152	if !found {
153		return errors.New("Mail Error: Invalid address header; Header: [" + header + "]")
154	}
155
156	// check to see if the addresses are valid
157	for i := range addresses {
158		address, err := mail.ParseAddress(addresses[i])
159		if err != nil {
160			return errors.New("Mail Error: " + err.Error() + "; Header: [" + header + "] Address: [" + addresses[i] + "]")
161		}
162
163		// check for more than one address
164		switch {
165		case header == "From" && len(email.from) > 0:
166			fallthrough
167		case header == "Sender" && len(email.sender) > 0:
168			fallthrough
169		case header == "Reply-To" && len(email.replyTo) > 0:
170			fallthrough
171		case header == "Return-Path" && len(email.returnPath) > 0:
172			return errors.New("Mail Error: There can only be one \"" + header + "\" address; Header: [" + header + "] Address: [" + addresses[i] + "]")
173		default:
174			// other address types can have more than one address
175		}
176
177		// save the address
178		switch header {
179		case "From":
180			email.from = address.Address
181		case "Sender":
182			email.sender = address.Address
183		case "Reply-To":
184			email.replyTo = address.Address
185		case "Return-Path":
186			email.returnPath = address.Address
187		default:
188			// check that the address was added to the recipients list
189			email.recipients, err = addAddress(email.recipients, address.Address)
190			if err != nil {
191				return errors.New("Mail Error: " + err.Error() + "; Header: [" + header + "] Address: [" + addresses[i] + "]")
192			}
193		}
194
195		// make sure the from and sender addresses are different
196		if email.from != "" && email.sender != "" && email.from == email.sender {
197			email.sender = ""
198			email.headers.Del("Sender")
199			return errors.New("Mail Error: From and Sender should not be set to the same address.")
200		}
201
202		// add all addresses to the headers except for Bcc and Return-Path
203		if header != "Bcc" && header != "Return-Path" {
204			// add the address to the headers
205			email.headers.Add(header, address.String())
206		}
207	}
208
209	return nil
210}
211
212// addAddress adds an address to the address list if it hasn't already been added
213func addAddress(addressList []string, address string) ([]string, error) {
214	// loop through the address list to check for dups
215	for _, a := range addressList {
216		if address == a {
217			return addressList, errors.New("Mail Error: Address: [" + address + "] has already been added")
218		}
219	}
220
221	return append(addressList, address), nil
222}
223
224type priority int
225
226const (
227	PriorityHigh priority = iota
228	PriorityLow
229)
230
231var priorities = [...]string{"High", "Low"}
232
233func (priority priority) String() string {
234	return priorities[priority]
235}
236
237// SetPriority sets the email message priority. Use with
238// either "High" or "Low".
239func (email *email) SetPriority(priority priority) error {
240	switch priority {
241	case PriorityHigh:
242		return email.AddHeaders(textproto.MIMEHeader{
243			"X-Priority":        {"1 (Highest)"},
244			"X-MSMail-Priority": {"High"},
245			"Importance":        {"High"},
246		})
247	case PriorityLow:
248		return email.AddHeaders(textproto.MIMEHeader{
249			"X-Priority":        {"5 (Lowest)"},
250			"X-MSMail-Priority": {"Low"},
251			"Importance":        {"Low"},
252		})
253	default:
254	}
255
256	return nil
257}
258
259// SetDate sets the date header to the provided date/time.
260// The format of the string should be YYYY-MM-DD HH:MM:SS Time Zone.
261//
262// Example: SetDate("2015-04-28 10:32:00 CDT")
263func (email *email) SetDate(dateTime string) error {
264	const dateFormat = "2006-01-02 15:04:05 MST"
265
266	// Try to parse the provided date/time
267	dt, err := time.Parse(dateFormat, dateTime)
268	if err != nil {
269		return errors.New("Mail Error: Setting date failed with: " + err.Error())
270	}
271
272	email.headers.Set("Date", dt.Format(time.RFC1123Z))
273
274	return nil
275}
276
277// SetSubject sets the subject of the email message.
278func (email *email) SetSubject(subject string) error {
279	return email.AddHeader("Subject", subject)
280}
281
282// SetBody sets the body of the email message.
283func (email *email) SetBody(contentType, body string) {
284	email.parts = []part{
285		part{
286			contentType: contentType,
287			body:        bytes.NewBufferString(body),
288		},
289	}
290}
291
292// Header adds the given "header" with the passed "value".
293func (email *email) AddHeader(header string, values ...string) error {
294	// check that there is actually a value
295	if len(values) < 1 {
296		return errors.New("Mail Error: no value provided; Header: [" + header + "]")
297	}
298
299	switch header {
300	case "Sender":
301		fallthrough
302	case "From":
303		fallthrough
304	case "To":
305		fallthrough
306	case "Bcc":
307		fallthrough
308	case "Cc":
309		fallthrough
310	case "Reply-To":
311		fallthrough
312	case "Return-Path":
313		return email.AddAddresses(header, values...)
314	case "Date":
315		if len(values) > 1 {
316			return errors.New("Mail Error: To many dates provided")
317		}
318		return email.SetDate(values[0])
319	default:
320		email.headers[header] = values
321	}
322
323	return nil
324}
325
326// Headers is used to add mulitple headers at once
327func (email *email) AddHeaders(headers textproto.MIMEHeader) error {
328	for header, values := range headers {
329		if err := email.AddHeader(header, values...); err != nil {
330			return err
331		}
332	}
333
334	return nil
335}
336
337// Alternative allows you to add alternative parts to the body
338// of the email message. This is most commonly used to add an
339// html version in addition to a plain text version that was
340// already added with SetBody.
341func (email *email) AddAlternative(contentType, body string) {
342	email.parts = append(email.parts,
343		part{
344			contentType: contentType,
345			body:        bytes.NewBufferString(body),
346		},
347	)
348}
349
350// Attach allows you to add an attachment to the email message.
351// You can optionally provide a different name for the file.
352func (email *email) AddAttachment(file string, name ...string) error {
353	if len(name) > 1 {
354		return errors.New("Mail Error: Attach can only have a file and an optional name")
355	}
356
357	return email.attach(file, false, name...)
358}
359
360// Inline allows you to add an inline attachment to the email message.
361// You can optionally provide a different name for the file.
362func (email *email) AddInline(file string, name ...string) error {
363	if len(name) > 1 {
364		return errors.New("Mail Error: Inline can only have a file and an optional name")
365	}
366
367	return email.attach(file, true, name...)
368}
369
370// attach does the low level attaching of the files
371func (email *email) attach(f string, inline bool, name ...string) error {
372	// Get the file data
373	data, err := ioutil.ReadFile(f)
374	if err != nil {
375		return errors.New("Mail Error: Failed to add file with following error: " + err.Error())
376	}
377
378	// get the file mime type
379	mimeType := mime.TypeByExtension(filepath.Ext(f))
380	if mimeType == "" {
381		mimeType = "application/octet-stream"
382	}
383
384	// get the filename
385	_, filename := filepath.Split(f)
386
387	// if an alternative filename was provided, use that instead
388	if len(name) == 1 {
389		filename = name[0]
390	}
391
392	if inline {
393		email.inlines = append(email.inlines, &file{
394			filename: filename,
395			mimeType: mimeType,
396			data:     data,
397		})
398	} else {
399		email.attachments = append(email.attachments, &file{
400			filename: filename,
401			mimeType: mimeType,
402			data:     data,
403		})
404	}
405
406	return nil
407}
408
409// getFrom returns the sender of the email, if any
410func (email *email) getFrom() string {
411	from := email.returnPath
412	if from == "" {
413		from = email.sender
414		if from == "" {
415			from = email.from
416			if from == "" {
417				from = email.replyTo
418			}
419		}
420	}
421
422	return from
423}
424
425func (email *email) hasMixedPart() bool {
426	return (len(email.parts) > 0 && len(email.attachments) > 0) || len(email.attachments) > 1
427}
428
429func (email *email) hasRelatedPart() bool {
430	return (len(email.parts) > 0 && len(email.inlines) > 0) || len(email.inlines) > 1
431}
432
433func (email *email) hasAlternativePart() bool {
434	return len(email.parts) > 1
435}
436
437// GetMessage builds and returns the email message
438func (email *email) GetMessage() string {
439	msg := newMessage(email)
440
441	if email.hasMixedPart() {
442		msg.openMultipart("mixed")
443	}
444
445	if email.hasRelatedPart() {
446		msg.openMultipart("related")
447	}
448
449	if email.hasAlternativePart() {
450		msg.openMultipart("alternative")
451	}
452
453	for _, part := range email.parts {
454		msg.addBody(part.contentType, part.body.Bytes())
455	}
456
457	if email.hasAlternativePart() {
458		msg.closeMultipart()
459	}
460
461	msg.addFiles(email.inlines, true)
462	if email.hasRelatedPart() {
463		msg.closeMultipart()
464	}
465
466	msg.addFiles(email.attachments, false)
467	if email.hasMixedPart() {
468		msg.closeMultipart()
469	}
470
471	return msg.getHeaders() + msg.body.String()
472}
473
474// Send sends the composed email
475func (email *email) Send(address string) error {
476	var auth smtp.Auth
477
478	from := email.getFrom()
479	if from == "" {
480		return errors.New(`Mail Error: No "From" address specified.`)
481	}
482
483	if len(email.recipients) < 1 {
484		return errors.New("Mail Error: No recipient specified.")
485	}
486
487	msg := email.GetMessage()
488
489	host, port, err := net.SplitHostPort(address)
490	if err != nil {
491		return errors.New("Mail Error: " + err.Error())
492	}
493
494	if email.Username != "" || email.Password != "" {
495		auth = smtp.PlainAuth("", email.Username, email.Password, host)
496	}
497
498	return send(host, port, from, email.recipients, msg, auth, email.Encryption, email.TLSConfig, email.ConnectTimeout)
499}
500
501// dial connects to the smtp server with the request encryption type
502func dial(host string, port string, encryption encryption, config *tls.Config) (*smtp.Client, error) {
503	var conn net.Conn
504	var err error
505	
506	address := host + ":" + port
507	
508	// do the actual dial
509	switch encryption {
510		case EncryptionSSL:
511			conn, err = tls.Dial("tcp", address, config)
512		default:
513			conn, err = net.Dial("tcp", address)
514	}
515	
516	if err != nil {
517		return nil, errors.New("Mail Error on dailing with encryption type " + encryption.String() + ": " + err.Error())
518	}
519
520	c, err := smtp.NewClient(conn, host)
521	
522	if err != nil {
523		return nil, errors.New("Mail Error on smtp dial: " + err.Error())
524	}
525	
526	return c, err
527}
528
529// smtpConnect connects to the smtp server and starts TLS and passes auth
530// if necessary
531func smtpConnect(host string, port string, from string, to []string, msg string, auth smtp.Auth, encryption encryption, config *tls.Config) (*smtp.Client, error) {
532	// connect to the mail server
533	c, err := dial(host, port, encryption, config)
534
535	if err != nil {
536		return nil, err
537	}
538
539	// send Hello
540	if err = c.Hello("localhost"); err != nil {
541		c.Close()
542		return nil, errors.New("Mail Error on Hello: " + err.Error())
543	}
544
545	// start TLS if necessary
546	if encryption == EncryptionTLS {
547		if ok, _ := c.Extension("STARTTLS"); ok {
548			if config.ServerName == "" {
549				config = &tls.Config{ServerName: host}
550			} 
551			
552			if err = c.StartTLS(config); err != nil {
553				c.Close()
554				return nil, errors.New("Mail Error on Start TLS: " + err.Error())
555			}
556		}
557	}
558
559	// pass the authentication if necessary
560	if auth != nil {
561		if ok, _ := c.Extension("AUTH"); ok {
562			if err = c.Auth(auth); err != nil {
563				c.Close()
564				return nil, errors.New("Mail Error on Auth: " + err.Error())
565			}
566		}
567	}
568
569	return c, nil
570}
571
572type smtpConnectErrorChannel struct {
573	client *smtp.Client
574	err    error
575}
576
577// send does the low level sending of the email
578func send(host string, port string, from string, to []string, msg string, auth smtp.Auth, encryption encryption, config *tls.Config, connectTimeout int) error {
579	var smtpConnectChannel chan smtpConnectErrorChannel
580	var c *smtp.Client = nil
581	var err error
582	
583	// set the timeout value
584	timeout := time.Duration(connectTimeout) * time.Second
585
586	// if there is a timeout, setup the channel and do the connect under a goroutine
587	if timeout != 0 {
588		smtpConnectChannel = make(chan smtpConnectErrorChannel, 2)
589		go func() {
590			c, err = smtpConnect(host, port, from, to, msg, auth, encryption, config)
591			// send the result
592			smtpConnectChannel <- smtpConnectErrorChannel{
593				client: c,
594				err:    err,
595			}
596		}()
597	}
598
599	if timeout == 0 {
600		// no timeout, just fire the connect
601		c, err = smtpConnect(host, port, from, to, msg, auth, encryption, config)
602	} else {
603		// get the connect result or timeout result, which ever happens first
604		select {
605		case result := <-smtpConnectChannel:
606			c = result.client
607			err = result.err
608		case <-time.After(timeout):
609			return errors.New("Mail Error: SMTP Connection timed out")
610		}
611	}
612
613	// check for connect error
614	if err != nil {
615		return err
616	}
617	
618	defer c.Close()
619
620	// Set the sender
621	if err := c.Mail(from); err != nil {
622		return err
623	}
624
625	// Set the recipients
626	for _, address := range to {
627		if err := c.Rcpt(address); err != nil {
628			return err
629		}
630	}
631
632	// Send the data command
633	w, err := c.Data()
634	if err != nil {
635		return err
636	}
637
638	// write the message
639	_, err = fmt.Fprint(w, msg)
640	if err != nil {
641		return err
642	}
643
644	err = w.Close()
645	if err != nil {
646		return err
647	}
648
649	return c.Quit()
650}
651
652type message struct {
653	headers  textproto.MIMEHeader
654	body     *bytes.Buffer
655	writers  []*multipart.Writer
656	parts    uint8
657	cids     map[string]string
658	charset  string
659	encoding encoding
660}
661
662func newMessage(email *email) *message {
663	return &message{
664		headers:  email.headers,
665		body:     new(bytes.Buffer),
666		cids:     make(map[string]string),
667		charset:  email.Charset,
668		encoding: email.Encoding}
669}
670
671func encodeHeader(text string, charset string, usedChars int) string {
672	// create buffer
673	buf := new(bytes.Buffer)
674	
675	// encode
676	encoder := header.NewEncoder(buf, charset, usedChars)
677	encoder.Encode([]byte(text))
678
679	return buf.String()
680	
681	/*
682		switch encoding {
683		case EncodingBase64:
684			return mime.BEncoding.Encode(charset, text)
685		default:
686			return mime.QEncoding.Encode(charset, text)
687	}
688	*/
689}
690
691// getHeaders returns the message headers
692func (msg *message) getHeaders() (headers string) {
693	// if the date header isn't set, set it
694	if date := msg.headers.Get("Date"); date == "" {
695		msg.headers.Set("Date", time.Now().Format(time.RFC1123Z))
696	}
697
698	// encode and combine the headers
699	for header, values := range msg.headers {
700		headers += header + ": " + encodeHeader(strings.Join(values, ", "), msg.charset, len(header) + 2) + "\r\n"
701	}
702
703	headers = headers + "\r\n"
704
705	return
706}
707
708// getCID gets the generated CID for the provided text
709func (msg *message) getCID(text string) (cid string) {
710	// set the date format to use
711	const dateFormat = "20060102.150405"
712
713	// get the cid if we have one
714	cid, exists := msg.cids[text]
715	if !exists {
716		// generate a new cid
717		cid = time.Now().Format(dateFormat) + "." + strconv.Itoa(len(msg.cids)+1) + "@mail.0"
718		// save it
719		msg.cids[text] = cid
720	}
721
722	return
723}
724
725// replaceCIDs replaces the CIDs found in a text string
726// with generated ones
727func (msg *message) replaceCIDs(text string) string {
728	// regular expression to find cids
729	re := regexp.MustCompile(`(src|href)="cid:(.*?)"`)
730	// replace all of the found cids with generated ones
731	for _, matches := range re.FindAllStringSubmatch(text, -1) {
732		cid := msg.getCID(matches[2])
733		text = strings.Replace(text, "cid:"+matches[2], "cid:"+cid, -1)
734	}
735
736	return text
737}
738
739// openMultipart creates a new part of a multipart message
740func (msg *message) openMultipart(multipartType string) {
741	// create a new multipart writer
742	msg.writers = append(msg.writers, multipart.NewWriter(msg.body))
743	// create the boundary
744	contentType := "multipart/" + multipartType + ";\n \tboundary=" + msg.writers[msg.parts].Boundary()
745
746	// if no existing parts, add header to main header group
747	if msg.parts == 0 {
748		msg.headers.Set("Content-Type", contentType)
749	} else { // add header to multipart section
750		header := make(textproto.MIMEHeader)
751		header.Set("Content-Type", contentType)
752		msg.writers[msg.parts-1].CreatePart(header)
753	}
754
755	msg.parts++
756}
757
758// closeMultipart closes a part of a multipart message
759func (msg *message) closeMultipart() {
760	if msg.parts > 0 {
761		msg.writers[msg.parts-1].Close()
762		msg.parts--
763	}
764}
765
766// base64Encode base64 encodes the provided text with line wrapping
767func base64Encode(text []byte) []byte {
768	// create buffer
769	buf := new(bytes.Buffer)
770
771	// create base64 encoder that linewraps
772	encoder := base64.NewEncoder(base64.StdEncoding, &base64LineWrap{writer: buf})
773
774	// write the encoded text to buf
775	encoder.Write(text)
776	encoder.Close()
777
778	return buf.Bytes()
779}
780
781// qpEncode uses the quoted-printable encoding to encode the provided text
782func qpEncode(text []byte) []byte {
783	// create buffer
784	buf := new(bytes.Buffer)
785	
786	encoder := quotedprintable.NewWriter(buf)
787	
788	encoder.Write(text)
789	encoder.Close()
790	
791	return buf.Bytes()
792}
793
794const maxLineChars = 76
795
796type base64LineWrap struct {
797	writer       io.Writer
798	numLineChars int
799}
800
801func (e *base64LineWrap) Write(p []byte) (n int, err error) {
802	n = 0
803	// while we have more chars than are allowed
804	for len(p)+e.numLineChars > maxLineChars {
805		numCharsToWrite := maxLineChars - e.numLineChars
806		// write the chars we can
807		e.writer.Write(p[:numCharsToWrite])
808		// write a line break
809		e.writer.Write([]byte("\r\n"))
810		// reset the line count
811		e.numLineChars = 0
812		// remove the chars that have been written
813		p = p[numCharsToWrite:]
814		// set the num of chars written
815		n += numCharsToWrite
816	}
817
818	// write what is left
819	e.writer.Write(p)
820	e.numLineChars += len(p)
821	n += len(p)
822
823	return
824}
825
826func (msg *message) write(header textproto.MIMEHeader, body []byte, encoding encoding) {
827	msg.writeHeader(header)
828	msg.writeBody(body, encoding)
829}
830
831func (msg *message) writeHeader(headers textproto.MIMEHeader) {
832	// if there are no parts add header to main headers
833	if msg.parts == 0 {
834		for header, value := range headers {
835			msg.headers[header] = value
836		}
837	} else { // add header to multipart section
838		msg.writers[msg.parts-1].CreatePart(headers)
839	}
840}
841
842func (msg *message) writeBody(body []byte, encoding encoding) {
843	// encode and write the body
844	switch encoding {
845	case EncodingQuotedPrintable:
846		msg.body.Write(qpEncode(body))
847	case EncodingBase64:
848		msg.body.Write(base64Encode(body))
849	default:
850		msg.body.Write(body)
851	}
852}
853
854func (msg *message) addBody(contentType string, body []byte) {
855	body = []byte(msg.replaceCIDs(string(body)))
856
857	header := make(textproto.MIMEHeader)
858	header.Set("Content-Type", contentType+"; charset="+msg.charset)
859	header.Set("Content-Transfer-Encoding", msg.encoding.String())
860	msg.write(header, body, msg.encoding)
861}
862
863var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
864
865func escapeQuotes(s string) string {
866	return quoteEscaper.Replace(s)
867}
868
869func (msg *message) addFiles(files []*file, inline bool) {
870	encoding := EncodingBase64
871	for _, file := range files {
872		header := make(textproto.MIMEHeader)
873		header.Set("Content-Type", file.mimeType+";\n \tname=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 6)+`"`)
874		header.Set("Content-Transfer-Encoding", encoding.String())
875		if inline {
876			header.Set("Content-Disposition", "inline;\n \tfilename=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 10)+`"`)
877			header.Set("Content-ID", "<"+msg.getCID(file.filename)+">")
878		} else {
879			header.Set("Content-Disposition", "attachment;\n \tfilename=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 10)+`"`)
880		}
881
882		msg.write(header, file.data, encoding)
883	}
884}