1.说明:
前段时间,一个人做一个类似"邮件群发器"的j2EE小项目,最终此项目不了了之,需要自己要有SMTP服务器!因为:
1).使用类似163(smtp.163.com)、QQ(smtp.qq.com)、sina(smtp.sina.com)这些免费的SMTP服务器,是做不了群发的(都有发量限制);
2).即使突破这个群发量限制(其实,突破是不太可能滴),使用别人的SMTP依然解决不了本项目的基本需求:邮件发送成功失败率统计(因为是中续转发:比如我要通过163邮箱发送到一QQ邮箱的朋友,我们是否成功发送到163邮箱,我们知道,可是163的SMTP是否成功发送到QQ的SMTP,我们就无法知道了)!中续转发的问题,这也是我们为什么想做"专业邮箱"来做销售、业务推广的原因。
2.所需资料包下载:
1).JavaMailAPI可到 http://java.sun.com/products/javamail/index.html 进行下载,并将mail.jar添加到classpath即可. 如果用J2EE就没必要非要用基本的JavaMailAPI了.J2EE的类就能处理了.
2).JAF框架可到 http://java.sun.com/products/javabeans/glasgow/jaf.html 进行下载,并将activation.jar添加到classpath即可.
3.开发中遇到的主要问题(下面相应代码有解决方案,好多是在网上找了很久很久才找到滴,东找一点西找一点,然后再慢慢调试出来):
1).邮件主题和附件标题乱码
2).处理邮件中的图片(处理后作为附件传送)
3).处理邮件中的链接(需要加密,以防服务器被刷)
4.我的相关代码:
1).实体Bean Email类:
package com.xx.email.bean;
import java.util.Date;
/**
*
* @author ydj
*
*/
public class Email {
/**
* 发送者地址
*/
private String from;
/**
*接收者地址
*/
private String to;
/**
*抄送地址
*/
private String ccTos[];
/**
*暗抄送地址
*/
private String bccTos[];
/**
*所有图片路径地址
*/
private String picturePaht[];
/**
*邮件主题
*/
private String subject;
/**
*邮件内容
*/
private String content;
/**
*发送时间
*/
private Date sentTime;
/**
*附件名称
*/
private String affixFileName;
/**
*是否抄送
*/
private boolean cc=false;
/**
*是否暗抄送
*/
private boolean bcc=false;
/**
*是否有附件
*/
private boolean fileAffix=false;
/**
*接收者地址
*/
private boolean picture=false;
public Email(boolean cc,boolean bcc,boolean fileAffix,boolean picture){
this.cc=cc;
this.bcc=bcc;
this.fileAffix=fileAffix;
this.picture=picture;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
2).创建邮件,发送邮件:
package com.xx.email.service.impl;
import java.util.Date;
import java.util.LinkedList;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.mail.AuthenticationFailedException;
import javax.mail.Authenticator;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
import com.yeexing.email.bean.Email;
import com.yeexing.email.struts.web.util.ByteArrayDataSource;
import com.yeexing.email.struts.web.util.ReadFileUtil;
/**
*
* @author ydj
*
*/
public class SendEmailService implements Runnable {
private String host = "smtp.qq.com";
private String prot = "25";
private boolean needAuth = true;
private String user;
private String password;
private Email email;
private static Message mimeMsg;
private static Session mailSession;
private static Properties props;
private static Multipart mp;
private Timer timer = new Timer();
static {
props = System.getProperties();
//获得邮件会话对象
mailSession = Session.getDefaultInstance(props, null);
//创建MIME邮件对象
mimeMsg = new MimeMessage(mailSession);
//related意味着可以发送html格式的邮件
mp = new MimeMultipart("related");
}
/**
*构造函数
*/
public SendEmailService() {
}
/**
* 构造函数
*
* @param email
* @param host
* @param user
* @param password
*/
public SendEmailService(Email email, String host, boolean needAuth,
String user, String password) {
this.email = email;
this.host = host;
this.needAuth = needAuth;
this.user = user;
this.password = password;
}
/**
*创建邮件
*
* @return
*/
private boolean createMimeMail() {
try {
mimeMsg.setFrom(new InternetAddress(email.getFrom()));
if (email.getTo() != null) {
mimeMsg.setRecipient(Message.RecipientType.TO,
new InternetAddress(email.getTo()));
}
if (email.isCc()) {
if (!setCcTos(email.getCcTos())) {
return false;
}
}
if (email.isBcc()) {
if (!setCopyBCCTos(email.getBccTos())) {
return false;
}
}
if (email.isPicture()) {
if (!processPicture(ReadFileUtil.readFile(email
.getPicturePaht()))) {
return false;
}
}
if (email.isFileAffix()) {
if (!addFileAffix(email.getAffixFileName())) {
return false;
}
}
mimeMsg.setSubject(MimeUtility.encodeText(email.getSubject(),
"GB2312", "B"));
BodyPart bodyPart = new MimeBodyPart();
if (email.getContent() == null) {
return false;
}
bodyPart.setContent(email.getContent(), "text/html;charset=GB2312");//正文
mp.addBodyPart(bodyPart);
Date sentTime = email.getSentTime();
if (sentTime == null || sentTime.before(new Date())) {
mimeMsg.setSentDate(new Date());
} else {
mimeMsg.setSentDate(sentTime);
}
mimeMsg.setHeader("Disposition-Notification-To", email.getFrom());//是否需要已读回执 ִ
mimeMsg.setHeader("Return-Receipt-To", email.getFrom());//判断邮件是否已经成功发送(时而不起作用)
mimeMsg.setContent(mp);
mimeMsg.saveChanges();
} catch (Exception e) {
System.err.println("����MIME�ʼ�����ʧ�ܣ�" + e);
}
return true;
}
/**
* 发送邮件
*
* @param from
* @param tos
* @param subject
* @param content
* return:0,发送成功;1,发送失败;2,连接邮件服务器失败
*/
private int sentEmail() {
//指定SMTP服务器
props.put("mail.smtp.host", host);
//指定SMTP服务器端口,默认25
if (!prot.equals("25")) {
props.put("mail.smtp.prot", prot);
}
//指定是否需要SMTP验证
if (needAuth) {
props.put("mail.smtp.auth", "true");
} else {
props.put("mail.smtp.auth", "false");
}
Transport transport = null;
try {
createMimeMail();
//是否在控制台显示debug调试信息
mailSession.setDebug(false);
transport = mailSession.getTransport("smtp");
try {
transport.connect(host, user, password);
} catch (AuthenticationFailedException e) {
System.out.println("连接邮件服务器错误:");
e.printStackTrace();
return 2;
} catch (MessagingException e) {
System.out.println("连接邮件服务器错误:");
return 2;
}
transport.sendMessage(mimeMsg, mimeMsg.getAllRecipients());
transport.close();
} catch (MessagingException e) {
System.out.println("发送邮件失败:");
e.printStackTrace();
Exception ex = null;
if ((ex = e.getNextException()) != null) {
System.out.println(ex.toString());
ex.printStackTrace();
}
return 1;
} finally {
try {
if (transport != null && transport.isConnected()) {
transport.close();
}
} catch (MessagingException e) {
// TODO Auto-generated catch block
System.out.println(e.toString());
}
}
return 0;
}
/**
* 定时发送
*/
public void start() {
timer.schedule(new TimerTask() {
public void run() {
sentEmail();
timer.cancel();
}
}, email.getSentTime());
}
/**
*即时发送
*/
public void run() {
// TODO Auto-generated method stub
sentEmail();
}
/**
* 添加附件
*
* @param filePath
* @param mp
* @return
*/
private boolean addFileAffix(String filePath) {
try {
BodyPart attachBodyPart = new MimeBodyPart();
FileDataSource fds = new FileDataSource(filePath);
attachBodyPart.setDataHandler(new DataHandler(fds));
attachBodyPart.setFileName("=?GBK?B?"
+ new sun.misc.BASE64Encoder().encode(fds.getName()
.getBytes()) + "?=");// 解决附件名中文乱码
mp.addBodyPart(attachBodyPart);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 添加抄送人
*
* @param tos
* @return
*/
private boolean setCcTos(String ccTos[]) {
if (ccTos == null) {
return false;
}
try {
for (int i = 0; i < ccTos.length; i++) {
mimeMsg.addRecipient(Message.RecipientType.CC,
new InternetAddress(ccTos[i]));
}
} catch (Exception e) {
return false;
}
return true;
}
/**
* 添加暗超送人
*
* @param mimeMsg
* @param copytos
* @return
*/
private boolean setCopyBCCTos(String[] copytos) {
if (copytos == null)
return false;
try {
for (int i = 0; i < copytos.length; i++) {
mimeMsg.addRecipient(Message.RecipientType.BCC,
new InternetAddress(copytos[i]));
}
return true;
} catch (Exception e) {
System.err.println("��Ӱ������˳�?" + e);
return false;
}
}
/**
* 处理邮件内容中的图片
*
* NOTE:在html中使用的方法是<img src ='cid:IMG[i]'/>
*
* @param list
*/
private boolean processPicture(LinkedList<byte[]> attachList) {
BodyPart mdp;
for (int i = 0; i < attachList.size(); i++) {
mdp = new MimeBodyPart(); //新建一个存放附件的BodyPart,将图片作为附件传送
DataHandler dh = new DataHandler(new ByteArrayDataSource(
(byte[]) attachList.get(i), "application/octet-stream"));
try {
mdp.setDataHandler(dh);
mdp.setHeader("Content-ID", "IMG" + new Integer(i).toString());
mp.addBodyPart(mdp);
} catch (MessagingException e) {
System.err.println("����ͼƬ��?" + e);
return false;
}
}
return true;
}
public void setAccount(String user, String password) {
this.user = user;
this.password = password;
}
public void setHost(String host) {
this.host = host;
}
public void setProt(String prot) {
this.prot = prot;
}
public void setNeedAuth(boolean needAuth) {
this.needAuth = needAuth;
}
public void setEmail(Email email) {
this.email = email;
}
/**
* test
*
* @param args
*/
public static void main(String[] args) {
}
}
/**
* Smtp认证
*/
class SmtpAuthenticator extends Authenticator {
String username = null;
String password = null;
// SMTP身份验证
public SmtpAuthenticator(String username, String password) {
this.username = username;
this.password = password;
}
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(this.username, this.password);
}
}
3).工具类1:
package com.yeexing.email.struts.web.util;
import java.io.*;
import javax.activation.*;
/**
* 处理图片(把图片转换后字节流)
* @author ydj
*
*/
public class ByteArrayDataSource implements DataSource {
/*** Data to write. */
private byte[] _data;
/*** Content-Type. */
private String _type;
/* Create a datasource from an input stream */
public ByteArrayDataSource(InputStream is, String type) {
_type = type;
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
int ch;
// XXX : must be made more efficient by
// doing buffered reads, rather than one byte reads
byte buffer[] = new byte[1024 * 1024];
while ((ch = is.read(buffer)) != -1)
os.write(ch);
_data = os.toByteArray();
} catch (IOException ioe) {
}
}
/* Create a datasource from a byte array */
public ByteArrayDataSource(byte[] data, String type) {
_data = data;
_type = type;
}
/* Create a datasource from a String */
public ByteArrayDataSource(String data, String type) {
try {
// Assumption that the string contains only ascii
// characters ! Else just pass in a charset into this
// constructor and use it in getBytes()
_data = data.getBytes("iso-8859-1");
} catch (UnsupportedEncodingException uee) {
}
_type = type;
}
public InputStream getInputStream() throws IOException {
if (_data == null)
throw new IOException("no data");
return new ByteArrayInputStream(_data);
}
public OutputStream getOutputStream() throws IOException {
throw new IOException("cannot do this");
}
public String getContentType() {
return _type;
}
public String getName() {
return "dummy";
}
}
} catch (UnsupportedEncodingException uee) {
}
_type = type;
}
public InputStream getInputStream() throws IOException {
if (_data == null)
throw new IOException("no data");
return new ByteArrayInputStream(_data);
}
public OutputStream getOutputStream() throws IOException {
throw new IOException("cannot do this");
}
public String getContentType() {
return _type;
}
public String getName() {
return "dummy";
}
}
4).工具类2:
package com.yeexing.email.struts.web.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
/**
* 处理html格式邮件内容工具类<br>
* 1.提取图片路径<br>
* 2.替换图片路径<br>
* 3.替换链接地址(含加密)<br>
* @author ydj
*
*/
public class ParseHtmlEmailUtil {
private ParseHtmlEmailUtil(){
}
/**
* 提取图片路径,此方法可改进
* @param resource
* @return
*/
public static String[] splitImagePath(String resource,HttpServletRequest request) {
if (resource == null || resource.length() == 0 || !resource.contains("<img")) {
return null;
}
StringBuffer sbf = new StringBuffer();
String[] fString = resource.split("<img");
String appPath=request.getSession(true).getServletContext().getRealPath("");
System.out.println("appPath1:"+appPath);
appPath=appPath.substring(0, appPath.lastIndexOf("//"));
System.out.println("appPath2:"+appPath);
for (int i = 0; i < fString.length; i++) {
System.out.println("fStirng_---" + fString[i]);
String s = fString[i].trim();
//System.out.println(":*********:" + s);
boolean check = s.contains("src");
if (s == null || s.length() == 0 || !check) {
continue;
}
String[] sString = s.split("src=/"");
//System.out.println("/n sString:" + sString.length);
for (int j = 0; j < sString.length; j++) {
String ss = sString[j].trim();
//System.out.println(":************:" + ss);
if (ss == null || ss.length() == 0) {
continue;
}
String second = sString[j].trim();
String[] tString = second.split("/"");
if (tString[0].contains(".")) {
sbf.append(appPath + tString[0]);
sbf.append(",");
}
}
}
return sbf.toString().split(",");
}
/**
*将Html文件中的<img src="第i张图片"/>转换为<img src="cid:IMGi"/>
* @param resource
* @return
*/
public static String replaceImagePath(String resource) {
Pattern pattern = Pattern.compile("(?i)<img(.*?)src=(.[^>]*)>",
Pattern.CASE_INSENSITIVE);
String tmpResource = "";
int course = 0;
Matcher matcher = pattern.matcher(resource);
int i = 0;
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
tmpResource += resource.substring(course, start);
System.out.println("tmpResource:" + tmpResource);
course = end;
String match = resource.substring(start, end);
//System.out.println("match:" + match);
String path = match
.replaceAll("(?i)<img(.*?)src='(.[^']*)'(.[^>]*)>", "$2")
.replaceAll("(?i)<img(.*?)src=/"(.[^/"]*)/"(.[^>]*)>", "$2")
.replaceAll("(?i)<img(.*?)src=/"(.[^/"]*)/"(.[^(]*)>", "$2")
.replaceAll("(?i)<img(.*?)src='(.[^']*)'(.*?)>", "$2")
.replaceAll("(?i)<img(.*?)src=(.[^//s]*)(.*?)>", "$2")
.replaceAll("(?i)<img(.*?)src=(.[^>]*)>", "$2");
//System.out.println("path:"+path);
path = match.replaceAll("/"", "").replaceAll("/'", "");
//System.out.println("�滻֮ǰpath:" + path);
path = path.replaceAll(path, "cid:IMG" + i);
//System.out.println("�滻֮��path:" + path);
tmpResource += "<img "+match.substring(5, match.indexOf("src"))+"src=/"" + path + "/"/>";
i++;
}
tmpResource+=resource.substring(course, resource.length());
System.out.println("***replaceImagePath�����滻������*****:" + tmpResource);
return tmpResource;
}
/**
*替换html中的超链接地址到web服务器地址
* @param resouce
* @return
*/
public static String replaceHrefPath(String resource){
Pattern pattern = Pattern.compile("(?i)<a(.*?)href=(.[^>]*)>",
Pattern.CASE_INSENSITIVE);
String tmpResource = "";
int course = 0;
Matcher matcher = pattern.matcher(resource);
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
tmpResource += resource.substring(course, start);
System.out.println("tmpResource:" + tmpResource);
course = end;
String match = resource.substring(start, end);
System.out.println("match:" + match);
String path = match
.replaceAll("(?i)<a(.*?)href='(.[^']*)'(.[^>]*)>", "$2")
.replaceAll("(?i)<a(.*?)href=/"(.[^/"]*)/"(.[^>]*)>", "$2")
.replaceAll("(?i)<a(.*?)href=/"(.[^/"]*)/"(.[^(]*)>", "$2")
.replaceAll("(?i)<a(.*?)href='(.[^']*)'(.*?)>", "$2")
.replaceAll("(?i)<a(.*?)href=(.[^//s]*)(.*?)>", "$2")
.replaceAll("(?i)<a(.*?)href=(.[^>]*)>", "$2");
path = path.replaceAll("/"", "").replaceAll("/'", "");
path = path.replaceAll(path, "http://192.168.0.11:8080/Email/hrefTracker2.do?realPath="+encodePath(path));
tmpResource += "<a href=/"" + path+"/">";
}
tmpResource+=resource.substring(course, resource.length());
System.out.println("***replaceHrefPath�����滻������:" + tmpResource);
return tmpResource;
}
/**
* 加密超链接地址
* @param path
* @return
*/
private static String encodePath(String path){
byte[] ret=null;
try {
ret = DESEncrypt.encrypt(path.getBytes(),DESEncrypt.REALPATH_CRYPT_KEY.getBytes());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String retstring =DESEncrypt.byte2hex(ret);
return retstring;
}
5).工具类3:
package com.xx.email.struts.web.util;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.LinkedList;
/**
* 根据文件路径或输入流,将其转化为字节数组
* @author ydj
*
*/
public class ReadFileUtil {
/**
* ��ȡ�ļ�
*
* @param filePath
* �ļ�¡¤��
* @return ���ض���������
*/
public static byte[] readFile(String filePath) {
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try {
fis = new FileInputStream(filePath);
bos = new ByteArrayOutputStream();
int bytesRead;
byte buffer[] = new byte[1024 * 1024];
while ((bytesRead = fis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
Arrays.fill(buffer, (byte) 0);
}
} catch (IOException e1) {
e1.printStackTrace();
} finally {
try {
if (bos != null)
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return bos.toByteArray();
}
/**
* ��ȡ�ļ�
*
* @param ins
* �ļ�������
* @return ���ض���������
*/
public static byte[]readFile(InputStream ins){
ByteArrayOutputStream bos=new ByteArrayOutputStream();
try {
int bytesRead;
byte buffer[]=new byte[1024*1024];
while((bytesRead=ins.read(buffer))!=-1){
bos.write(buffer, 0, bytesRead);
Arrays.fill(buffer, (byte)0);
}
} catch (IOException e) {
e.printStackTrace();
}finally{
if(bos!=null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return bos.toByteArray();
}
/**
* �������ļ�
* @param filePath �ļ�¡¤��
* @return ���ض����������б�
*/
public static LinkedList<byte[]> readFile(String filePath[]){
LinkedList<byte[]> affixList=new LinkedList<byte[]>();
for(int i=0;i<filePath.length;i++){
affixList.add(readFile(filePath[i]));
}
return affixList;
}
}
6).工具类4:
package com.xx.email.struts.web.util;
import java.security.*;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
/**
* 加密解密
* @author ydj
*
*/
public class DESEncrypt {
public static String REALPATH_CRYPT_KEY = "?jkfddo(F%$<>MKHKO";
public static String DES = "DES";
/**
* 加密
*
* @param src
* 数据�?
* @param key
* 密钥,长度必须是8的�?�数
* @return 返回加密后的数据
* @throws Exception
*/
public static byte[] encrypt(byte[] src, byte[] key) throws Exception {
// DES算法要求有一个可信任的随机数�?
SecureRandom sr = new SecureRandom();
// 从原始密匙数据创建DESKeySpec对象
DESKeySpec dks = new DESKeySpec(key);
// 创建�?个密匙工厂,然后用它把DESKeySpec转换�?
// �?个SecretKey对象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
SecretKey securekey = keyFactory.generateSecret(dks);
// Cipher对象实际完成加密操作
Cipher cipher = Cipher.getInstance(DES);
// 用密匙初始化Cipher对象
cipher.init(Cipher.ENCRYPT_MODE, securekey, sr);
// 现在,获取数据并加密
// 正式执行加密操作
return cipher.doFinal(src);
}
/**
* 解密
*
* @param src
* 数据�?
* @param key
* 密钥,长度必须是8的�?�数
* @return 返回解密后的原始数据
* @throws Exception
*/
public static byte[] decrypt(byte[] src, byte[] key) throws Exception {
// DES算法要求有一个可信任的随机数�?
SecureRandom sr = new SecureRandom();
// 从原始密匙数据创建一个DESKeySpec对象
DESKeySpec dks = new DESKeySpec(key);
// 创建�?个密匙工厂,然后用它把DESKeySpec对象转换�?
// �?个SecretKey对象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
SecretKey securekey = keyFactory.generateSecret(dks);
// Cipher对象实际完成解密操作
Cipher cipher = Cipher.getInstance(DES);
// 用密匙初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, securekey, sr);
// 现在,获取数据并解密
// 正式执行解密操作
return cipher.doFinal(src);
}
/**
* 密码解密
*
* @param data
* @return
* @throws Exception
*/
public final static String decrypt(String data) {
try {
return new String(decrypt(hex2byte(data.getBytes()),
REALPATH_CRYPT_KEY.getBytes()));
} catch (Exception e) {
}
return null;
}
/**
* 密码加密
*
* @param src
* @return
* @throws Exception
*/
public final static String encrypt(String src) {
try {
return byte2hex(encrypt(src.getBytes(), REALPATH_CRYPT_KEY
.getBytes()));
} catch (Exception e) {
}
return null;
}
public static String byte2hex(byte[] b) {
String hs = "";
String stmp = "";
for (int n = 0; n < b.length; n++) {
stmp = (java.lang.Integer.toHexString(b[n] & 0XFF));
if (stmp.length() == 1){
hs = hs + "0" + stmp;
}else{
hs = hs + stmp;
}
}
return hs.toUpperCase();
}
public static byte[] hex2byte(byte[] b) {
if ((b.length % 2) != 0){
throw new IllegalArgumentException("长度不是偶数");
}
byte[] b2 = new byte[b.length / 2];
for (int n = 0; n < b.length; n += 2) {
String item = new String(b, n, 2);
b2[n / 2] = (byte) Integer.parseInt(item, 16);
}
return b2;
}
5.参考资料:
说一说,看一看,群发的问题:
http://www.168318.com/bbs/ShowPost.asp?ThreadID=355
6.参考资料:
1).http://www.matrix.org.cn/resource/article/44/44101_JavaMail.html
2). http://qkzz.net/magazine/1009-3044/2007/26/2231998.htm
3). http://www.yx361.com/yingxiaowenku/
4). http://tech.ddvip.com/2006-11/116437866112212.html
5). http://book.8844.com/ViewEntry.aspx?EntryID=34160
6 ). http://www.5dmail.net/