php emailing a .pdf attachment comes out corrupt

hi there,
im struggeling!
Trying to mail a pdf file via a php script. ive tried about 3 different scripts which attach the pdf but when i try to open it in my mail its always blank. Its oviouly fine on the server.
im running wamp server.
its running via a smtp server that is configured in the php.ini

as per this line in the index file its using the correct mime:
  $msg->Attach("C:\wamp\www\bradmail\dad.pdf", "application/pdf");


In the code box you will find the index.php file as well as the class file.

any suggestions please really desperate.
HEREWITH INDEX.PHP FILE:
 
<?
//** load email class definition.
 
  include('class.Email.php');  
 
//** establish to,from, and any other recipiants.
 
  $Sender = "you@there.com";
  $Recipiant = "david@pcnetwork.co.za";
  $Cc = "";
  $Bcc = "";
 
//** create the text and HTML versions of the body content here.
 
  $textVersion = "Hello World!";
  $htmlVersion = "<font face='verdana' color='blue'><b>Hello World</b></font>";
 
  unset($msg);
 
//** !!!! SEND A PLAIN TEXT EMAIL !!!!
//** create the new message using the to, from, and email subject.
 
  $msg = new Email($Recipiant, $Sender, "A Test Plain Text Email!"); 
  $msg->Cc = $Cc;
  $msg->Bcc = $Bcc;
 
//** set the message to be text only and set the email content.
 
  $msg->TextOnly = true;
  $msg->Content = $textVersion;
  
//** send the email message.
 
  $SendSuccess = $msg->Send();
  
  echo "Plain text email was ", 
       ($SendSuccess ? "sent" : "not sent"), "<br>";
 
  unset($msg);
 
//** !!!! SEND AN HTML EMAIL w/ATTACHMENT !!!!
//** create the new message using the to, from, and email subject.
 
  $msg = new Email($Recipiant, $Sender, "A Test HTML Email!");
  $msg->Cc = $Cc;
  $msg->Bcc = $Bcc;
 
//** set the message to be text only and set the email content.
 
  $msg->TextOnly = false;
  $msg->Content = $htmlVersion;
 
//** attach this scipt itself to the message.
 
  $msg->Attach("C:\wamp\www\bradmail\dad.pdf", "application/pdf");
 
//** send the email message.
 
  $SendSuccess = $msg->Send();
 
  echo "HTML email w/attachment was ",
       ($SendSuccess ? "sent" : "not sent"), "<br>";
 
  unset($msg);
 
//** !!!! SEND A MULTIPART/ALTERNATIVE EMAIL !!!!
//** create the new message using the to, from, and email subject.
 
  $msg = new Email($Recipiant, $Sender, "A Test Multipart Alternative Email!");
  $msg->Cc = $Cc;
  $msg->Bcc = $Bcc;
 
//** set the message to be a multiprat/alternative email. This allows for
//** multiple versions of same content.
//** NOTE: you cannot send attachments when a message is set to be
//**       multipart/alternative. there is also no way to switch
//**       back to normal multipart/mixed after this call.
 
  $msg->SetMultipartAlternative($textVersion, $htmlVersion);
 
//** send the email message.
 
  $SendSuccess = $msg->Send();
 
  echo "Multipart/alternative email was ",
       ($SendSuccess ? "sent" : "not sent"), "<br>";
 
?>
 
 
 
 
HEREWITH CLASS FILE:
 
 
 
 
 
<?
//** ©William Fowler (wmfwlr@cogeco.ca) 
//** MAY 13/2004, Version 1.1
//** - added support for CC and BCC fields.
//** - added support for multipart/alternative messages.
//** - added ability to create attachments manually using literal content.
//** DECEMBER 15/2003, Version 1.0 
 
  if(isset($GLOBALS["emailmsgclass_php"])) { return; }  //** onlyinclude once. 
  $GLOBALS["emailmsgclass_php"] = 1;                    //** filewas included. 
 
//** the newline character(s) to be used when generating an email message. 
 
  define("EmailNewLine", "\r\n"); 
 
//** the unique X-Mailer identifier for emails sent with this tool. 
 
  define("EmailXMailer", "PHP-EMAIL,v1.1 (William Fowler)"); 
 
//** the default charset values for both text and HTML emails. 
 
  define("DefaultCharset", "iso-8859-1"); 
 
//**!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
//**EMAIL_MESSAGE_CLASS_DEFINITION******************************************** 
//** The Email class wrappers PHP's mail function into a class capable of 
//** sending attachments and HTML email messages. Custom headers can also be 
//** included as if using the mail function. 
 
class Email 
{ 
//** (String) the recipiant email address, or comma separated addresses. 
 
  var $To = null; 
 
//** (String) the recipiant addresses to receive a copy. Can be a comma
//** separated addresses. 
 
  var $Cc = null; 
 
//** (String) the recipiant addresses to receive a hidden copy. Can be a 
//** comma separated addresses. 
 
  var $Bcc = null; 
 
//** (String) the email address of the message sender. 
 
  var $From = null; 
 
//** (String) the subject of the email message. 
 
  var $Subject = null; 
 
//** (String) body content for the message. Must be plain text or HTML based
//** on the 'TextOnly' field. This field is ignored if 
//** SetMultipartAlternative() is called with valid content.
 
  var $Content = null;
 
//** an array of EmailAttachment instances to be sent with this message. 
 
  var $Attachments; 
 
//** any custom header information that must be used when sending email. 
 
  var $Headers = null; 
 
//** whether email to be sent is a text email or a HTML email. 
 
  var $TextOnly = true; 
 
//** the charset of the email to be sent (initially none, let type decide). 
 
  var $Charset = null; 
 
//** Create a new email message with the parameters provided. 
 
  function Email($to=null, $from=null, $subject=null, $headers=null) 
  { 
    $this->To = $to; 
    $this->From = $from; 
    $this->Subject = $subject; 
    $this->Headers = $headers;   
 
//** create an empty array for attachments. NULL out attachments used for
//** multipart/alternative messages initially.
   
    $this->Attachments = Array();    
    $this->Attachments["text"] = null;
    $this->Attachments["html"] = null;
  } 
//** Returns: Boolean
//** Set this email message to contain both text and HTML content.
//** If successful all attachments and content are ignored.
 
  function SetMultipartAlternative($text=null, $html=null)
  {
//** non-empty content for the text and HTML version is required.
 
    if(strlen(trim(strval($html))) == 0 || strlen(trim(strval($text))) == 0)
      return false;
    else
    {
//** create the text email attachment based on the text given and the standard
//** plain text MIME type.
 
      $this->Attachments["text"] = new EmailAttachment(null, "text/plain");
      $this->Attachments["text"]->LiteralContent = strval($text);
 
//** create the html email attachment based on the HTML given and the standard
//** html text MIME type.
 
      $this->Attachments["html"] = new EmailAttachment(null, "text/html");
      $this->Attachments["html"]->LiteralContent = strval($html);
 
      return true;  //** operation was successful.
    }
  }
//** Returns: Boolean 
//** Create a new file attachment for the file (and optionally MIME type) 
//** given. If the file cannot be located no attachment is created and 
//** FALSE is returned. 
 
  function Attach($pathtofile, $mimetype=null) 
  { 
//** create the appropriate email attachment. If the attachment does not 
//** exist the attachment is not created and FALSE is returned. 
 
    $attachment = new EmailAttachment($pathtofile, $mimetype); 
    if(!$attachment->Exists()) 
      return false; 
    else 
    { 
      $this->Attachments[] = $attachment;  //** add the attachment to list. 
      return true;                         //** attachment successfully added. 
    } 
  } 
//** Returns: Boolean 
//** Determine whether or not the email message is ready to be sent. A TO and 
//** FROM address are required. 
 
  function IsComplete() 
  { 
    return (strlen(trim($this->To)) > 0 && strlen(trim($this->From)) > 0); 
  } 
//** Returns: Boolean 
//** Attempt to send the email message. Attach all files that are currently 
//** valid. Send the appropriate text/html message. If not complete FALSE is 
//** returned and no message is sent. 
 
  function Send() 
  { 
    if(!$this->IsComplete())  //** message is not ready to send. 
      return false;           //** no message will be sent. 
 
//** generate a unique boundry identifier to separate attachments. 
 
    $theboundary = "-----" . md5(uniqid("EMAIL")); 
 
//** the from email address and the current date of sending. 
 
    $headers = "Date: " . date("r", time()) . EmailNewLine .
               "From: $this->From" . EmailNewLine;
 
//** if a non-empty CC field is provided add it to the headers here.
 
    if(strlen(trim(strval($this->Cc))) > 0)
      $headers .= "CC: $this->Cc" . EmailNewLine;
    
//** if a non-empty BCC field is provided add it to the headers here.
 
    if(strlen(trim(strval($this->Bcc))) > 0)
      $headers .= "BCC: $this->Bcc" . EmailNewLine;
 
//** add the custom headers here, before important headers so that none are 
//** overwritten by custom values. 
 
    if($this->Headers != null && strlen(trim($this->Headers)) > 0) 
      $headers .= $this->Headers . EmailNewLine; 
 
//** determine whether or not this email is mixed HTML and text or both.
 
    $isMultipartAlternative = ($this->Attachments["text"] != null &&
                               $this->Attachments["html"] != null);
 
//** determine the correct MIME type for this message.
 
    $baseContentType = "multipart/" . ($isMultipartAlternative ? 
                                       "alternative" : "mixed");
 
//** add the custom headers, the MIME encoding version and MIME typr for the 
//** email message, the boundry for attachments, the error message if MIME is 
//** not suppported. 
 
    $headers .= "X-Mailer: " . EmailXMailer . EmailNewLine . 
                "MIME-Version: 1.0" . EmailNewLine . 
                "Content-Type: $baseContentType; " .
                "boundary=\"$theboundary\"" . EmailNewLine . EmailNewLine; 
 
//** if a multipart message add the text and html versions of the content.
 
    if($isMultipartAlternative)
    {
//** add the text and html versions of the email content.
 
      $thebody = "--$theboundary" . EmailNewLine . 
                  $this->Attachments["text"]->ToHeader() . EmailNewLine .
                 "--$theboundary" . EmailNewLine . 
                  $this->Attachments["html"]->ToHeader() . EmailNewLine; 
    }
//** if either only html or text email add the content to the email body.
 
    else
    {
//** determine the proper encoding type and charset for the message body. 
 
      $theemailtype = "text/" . ($this->TextOnly ? "plain" : "html"); 
      if($this->Charset == null) 
        $this->Charset = DefaultCharset;
 
//** add the encoding header information for the body to the content. 
 
      $thebody = "--$theboundary" . EmailNewLine . 
                 "Content-Type: $theemailtype; charset=$this->Charset" . 
                  EmailNewLine . "Content-Transfer-Encoding: 8bit" . 
                  EmailNewLine . EmailNewLine . $this->Content . 
                  EmailNewLine . EmailNewLine; 
 
//** loop over the attachments for this email message and attach the files 
//** to the email message body. Only if not multipart alternative.
 
      foreach($this->Attachments as $attachment) 
      { 
 //** check for NULL attachments used by multipart alternative emails. Do not
 //** attach these.
 
        if($attachment != null)
        {
          $thebody .= "--$theboundary" . EmailNewLine . 
                       $attachment->ToHeader() . EmailNewLine; 
        }
      } 
    }
//** end boundry marker is required.
 
    $thebody .= "--$theboundary--"; 
 
//** attempt to send the email message. Return the operation success. 
 
    return mail($this->To, $this->Subject, $thebody, $headers); 
  } 
} 
//******************************************END_EMAIL_MESSAGE_CLASS_DEFINITION 
//**!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
 
//**!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
//**EMAIL_ATTACHMENT_CLASS_DEFINITION***************************************** 
//** The EmailAttachment class links a file in the file system to the 
//** appropriate header to be included in an email message. if the file does 
//** not exist the attachment will not be sent in any email messages. It can
//** also be used to generate an attachment from literal content provided.
 
class EmailAttachment 
{ 
//** (String) the full path to the file to be attached. 
 
  var $FilePath = null; 
 
//** (String) the MIME type for the file data of this attachment. 
 
  var $ContentType = null; 
 
//** binary content to be used instead the contents of a file.
 
  var $LiteralContent = null;
 
//** Creates a new email attachment ffrom the file path given. If no content 
//** type is given the default 'application/octet-stream' is used. 
 
  function EmailAttachment($pathtofile=null, $mimetype=null) 
  { 
//** if no MIME type is provided use the default value specifying binary data. 
//** Otherwise use the MIME type provided. 
 
    if($mimetype == null || strlen(trim($mimetype)) == 0) 
      $this->ContentType = "application/octet-stream"; 
    else 
      $this->ContentType = $mimetype; 
 
    $this->FilePath = $pathtofile;  //** save the path to the file attachment. 
  } 
//** Returns: Boolean
//** Determine whether literal content is provided and should be used as the
//** attachment rather than a file.
 
  function HasLiteralContent()
  {
    return (strlen(strval($this->LiteralContent)) > 0);
  }
//** Returns: String 
//** Get the binary string data to be used as this attachment. If literal
//** content is provided is is used, otherwise the contents of the file path
//** for this attachment is used. If no content is available NULL is returned.
 
  function GetContent()
  {
//** non-empty literal content is available. Use that as the attachment.
//** Assume the user has used correct MIME type.
 
    if($this->HasLiteralContent())
      return $this->LiteralContent;
 
//** no literal content available. Try to get file data.
 
    else
    {
      if(!$this->Exists())  //** file does not exist.
        return null;        //** no content is available.
      else
      {
//** open the file attachment in binary mode and read the contents. 
 
        $thefile = fopen($this->FilePath, "rb"); 
        $data = fread($thefile, filesize($this->FilePath));
        fclose($thefile);
        return $data; 
      }
    }
  }
//** Returns: Boolean 
//** Determine whether or not the email attachment has a valid, existing file 
//** associated with it. 
 
  function Exists() 
  { 
    if($this->FilePath == null || strlen(trim($this->FilePath)) == 0) 
      return false; 
    else 
      return file_exists($this->FilePath); 
  } 
//** Returns: String 
//** Generate the appropriate header string for this email attachment. If the 
//** the attachment content does not exist NULL is returned. 
 
  function ToHeader() 
  { 
    $attachmentData = $this->GetContent();  //** get content for the header.
    if($attachmentData == null)             //** no valid attachment content.
      return null;                          //** no header can be generted. 
 
//** add the content type and file name of the attachment. 
 
    $header = "Content-Type: $this->ContentType;"; 
 
//** if an attachment then add the appropriate disposition and file name(s).
  
    if(!$this->HasLiteralContent())
    {
      $header .= " name=\"" . basename($this->FilePath) . "\"" . EmailNewLine .
                 "Content-Disposition: attachment; filename=\"" . 
                  basename($this->FilePath) . "\""; 
    }
    $header .= EmailNewLine;
 
//** add the key for the content encoding of the attachment body to follow. 
 
    $header .= "Content-Transfer-Encoding: base64" . EmailNewLine . 
                EmailNewLine; 
 
//** add the attachment data to the header. encode the binary data in BASE64 
//** and break the encoded data into the appropriate chunks. 
 
    $header .= chunk_split(base64_encode($attachmentData), 76, EmailNewLine) .
               EmailNewLine; 
 
    return $header;  //** return the headers generated by file. 
  } 
} 
//***************************************END_EMAIL_ATTACHMENT_CLASS_DEFINITION 
//**!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
?>

Open in new window

davidvanrensburgAsked:
Who is Participating?
 
davidvanrensburgConnect With a Mentor Author Commented:
i fixed the problem myself this line must read true and not false when attaching a file and not reading a file into the script,,

$mime->addAttachment('dad.pdf', 'application/pdf', 'dad.pdf', true);

thanks for you help
0
 
shobinsunCommented:
Hi,

Download the latest version of fpdf class from:

http://www.fpdf.org/

And do the code:

This is a simple example to create and send pdf mail using fpdf.

Hope this will help you.

Thanks and Regards

<?php
/*
 * Created on Mar 16, 2009
 *
 * To change the template for this generated file go to
 * Window - Preferences - PHPeclipse - PHP - Code Templates
 */
 
require( 'fpdf16/fpdf.php');
 	
$pdf=new FPDF();
$pdf->AddPage();
$pdf->SetFont('Arial','B',16);
$pdf->Cell(40,10,'Hello World!');
$pdfcontent = $pdf->Output("Report.pdf", "S");
 
require_once('Mail.php');
require_once('Mail/mime.php');
 
$to = 'shobinsun@gmail.com';;
//echo "<br>To:",$to;
// email address of the sender
 
// subject of the email
$subject = "Grade Report";
 
$from = 'sh@example.com';
// email header format complies the PEAR's Mail class
// this header includes sender's email and subject
$headers = array('From' => $from,
'Subject' => $subject);
 
// We will send this email as HTML format
// which is well presented and nicer than plain text
// using the heredoc syntax
// REMEMBER: there should not be any space after PDFMAIL keyword
$htmlMessage = <<<PDFMAIL
<html>
<body bgcolor="#ffffff">
<p align="center">
Please find the pdf attached in the email.
</p>
</body>
</html>
PDFMAIL;
 
// create a new instance of the Mail_Mime class
$mime = new Mail_Mime();
 
// set HTML content
$mime->setHtmlBody($htmlMessage);
 
// IMPORTANT: add pdf content as attachment
$mime->addAttachment($pdfcontent, 'application/pdf', 'Report.pdf', false, 'base64');
 
// build email message and save it in $body
$body = $mime->get();
 
// build header
$hdrs = $mime->headers($headers);
 
// create Mail instance that will be used to send email later
$mail = &Mail::factory('mail');
 
// Sending the email, according to the address in $to,
// the email headers in $hdrs,
// and the message body in $body.
$mail->send($to, $hdrs, $body);
 
?>

Open in new window

0
 
davidvanrensburgAuthor Commented:
thats not going to help me as i have the pdf file already on the server.
There must be a reliable way to mail a pdf off from a php script.
0
Upgrade your Question Security!

Your question, your audience. Choose who sees your identity—and your question—with question security.

 
davidvanrensburgAuthor Commented:
increasing points
0
 
shobinsunCommented:
Hi,

You can do without creating the pdf from fpdf class. just see the following example:

Regards
<?php
/*
 * Created on Mar 16, 2009
 *
 * To change the template for this generated file go to
 * Window - Preferences - PHPeclipse - PHP - Code Templates
 */
 
require_once('Mail.php');
require_once('Mail/mime.php');
 
$to = 'shobinsun@gmail.com';;
//echo "<br>To:",$to;
// email address of the sender
 
// subject of the email
$subject = "Grade Report";
 
$pdfcontent = 'Report.pdf';
 
$from = 'sh@example.com';
// email header format complies the PEAR's Mail class
// this header includes sender's email and subject
$headers = array('From' => $from,
'Subject' => $subject);
 
// We will send this email as HTML format
// which is well presented and nicer than plain text
// using the heredoc syntax
// REMEMBER: there should not be any space after PDFMAIL keyword
$htmlMessage = <<<PDFMAIL
<html>
<body bgcolor="#ffffff">
<p align="center">
Please find the pdf attached in the email.
</p>
</body>
</html>
PDFMAIL;
 
// create a new instance of the Mail_Mime class
$mime = new Mail_Mime();
 
// set HTML content
$mime->setHtmlBody($htmlMessage);
 
// IMPORTANT: add pdf content as attachment
$mime->addAttachment($pdfcontent, 'application/pdf', 'Report.pdf', false, 'base64');
 
// build email message and save it in $body
$body = $mime->get();
 
// build header
$hdrs = $mime->headers($headers);
 
// create Mail instance that will be used to send email later
$mail = &Mail::factory('mail');
 
// Sending the email, according to the address in $to,
// the email headers in $hdrs,
// and the message body in $body.
$mail->send($to, $hdrs, $body);
 
?>

Open in new window

0
 
davidvanrensburgAuthor Commented:
ive just tried that and get the following error:
Parse error: syntax error, unexpected T_STRING in C:\wamp\www\brad2\Mail.php on line 1

i downloaded mime and mail from the pear webside and put the files in the same folder.
line 1 of the mail.php pear prigram states the following:
package.xml

0
 
shobinsunCommented:
Hi,

Give me that line
0
 
davidvanrensburgAuthor Commented:
package.xml
0
 
davidvanrensburgAuthor Commented:
i could have the wrong 2 class files?

im using http://pear.php.net/packages.php?catpid=14&catname=Mail&php=all
no 1 and no 5
0
 
shobinsunCommented:
Hi,

Please download the following and copy the folder in to your directory and use that:


<?php
/*
 * Created on Mar 16, 2009
 *
 * To change the template for this generated file go to
 * Window - Preferences - PHPeclipse - PHP - Code Templates
 */
 
require_once('pear/Mail.php');
require_once('pear/Mail/mime.php');
 
$to = 'shobinsun@gmail.com';;
//echo "<br>To:",$to;
// email address of the sender
 
// subject of the email
$subject = "Grade Report";
 
$pdfcontent = 'Report.pdf';
 
$from = 'sh@example.com';
// email header format complies the PEAR's Mail class
// this header includes sender's email and subject
$headers = array('From' => $from,
'Subject' => $subject);
 
// We will send this email as HTML format
// which is well presented and nicer than plain text
// using the heredoc syntax
// REMEMBER: there should not be any space after PDFMAIL keyword
$htmlMessage = <<<PDFMAIL
<html>
<body bgcolor="#ffffff">
<p align="center">
Please find the pdf attached in the email.
</p>
</body>
</html>
PDFMAIL;
 
// create a new instance of the Mail_Mime class
$mime = new Mail_Mime();
 
// set HTML content
$mime->setHtmlBody($htmlMessage);
 
// IMPORTANT: add pdf content as attachment
$mime->addAttachment($pdfcontent, 'application/pdf', 'Report.pdf', false, 'base64');
 
// build email message and save it in $body
$body = $mime->get();
 
// build header
$hdrs = $mime->headers($headers);
 
// create Mail instance that will be used to send email later
$mail = &Mail::factory('mail');
 
// Sending the email, according to the address in $to,
// the email headers in $hdrs,
// and the message body in $body.
$mail->send($to, $hdrs, $body);
 
?>

Open in new window

0
 
shobinsunCommented:
sory I forgot to copy the link:

Please download the following and copy the folder in to your directory and use that:

http://rapidshare.com/files/224809815/pear.zip.html

Regards
0
 
davidvanrensburgAuthor Commented:
yes that is what i am using but as you can see in the code:

require_once('pear/Mail.php');
require_once('pear/Mail/mime.php');


it relies on pears 2 mail functions and that is what it is complaining about.
0
 
davidvanrensburgAuthor Commented:
keeps telling me that my session has expired in rapidshare after i have to wait the 30 seconds as im not a premium member.

Please would you try mail me the pair or can you upload it elsewhere please or let me have another link?

this is the problem i think.
i really apprecate it please
0
 
shobinsunCommented:
Hi,

In that Mail.php there is no line "package.xml"

Did you try with it?

0
 
shobinsunCommented:
Hi,

Please give your mail Id
0
 
davidvanrensburgAuthor Commented:
david@pcnetwork.co.za
0
 
shobinsunCommented:
Hi,

Please find the mail.

Regards
0
 
davidvanrensburgAuthor Commented:
hi im recieving a pdf in mail now but its doing the same thing.
its corrupted.

the error i get when i try to open the pdf is..

adobe reader could not open dad.pdf because either its not a supported file or its damaged.
i changed this line to.
$mime->addAttachment($dad.pdf, 'application/pdf', 'dad.pdf', false, 'base64');
0
 
shobinsunCommented:
Hi,

Is the pdf you sent is a valid pdf?

Please send a mail to my email id..with this.

I will send you a valid pdf document.

Thanks and Regards
0
 
davidvanrensburgAuthor Commented:
its a valid pdf as i can open it perfectly just not after its emailed.
why is this?
0
 
shobinsunCommented:
Hi,

Please send a mail to me with the pdf file (I have sent you) with this code.
0
 
shobinsunCommented:
Hi,

Happy that you have solved the problem. I have sent successfully with false option too and can view the pdf content.

Regards
0
Question has a verified solution.

Are you are experiencing a similar issue? Get a personalized answer when you ask a related question.

Have a better answer? Share it in a comment.

All Courses

From novice to tech pro — start learning today.