package org.subethamail.entity; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Serializable; import java.sql.Timestamp; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.Vector; import java.util.logging.Level; import javax.ejb.EJBException; import javax.mail.MessagingException; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.OneToMany; import javax.persistence.QueryHint; import lombok.extern.java.Log; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.Index; import org.hibernate.annotations.IndexColumn; import org.hibernate.annotations.Sort; import org.hibernate.annotations.SortType; import org.hibernate.annotations.Table; import org.hibernate.search.annotations.Boost; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.Indexed; import org.subethamail.common.SubEthaMessage; import org.subethamail.common.TimeUtils; import org.subethamail.entity.i.Validator; /** * Entity for a single piece of mail. * * @author Jeff Schnitzer */ @NamedQueries({ @NamedQuery( name="MailByMessageId", query="from Mail m where m.list.id = :listId and m.messageId = :messageId", hints={ @QueryHint(name="org.hibernate.readOnly", value="true"), @QueryHint(name="org.hibernate.cacheable", value="true") } ), @NamedQuery( name="MailByList", query="from Mail m where m.list.id = :listId and m.hold is null order by m.sentDate desc", hints={ @QueryHint(name="org.hibernate.readOnly", value="true"), @QueryHint(name="org.hibernate.cacheable", value="true") } ), @NamedQuery( name="CountMailByList", query="select count(*) from Mail m where m.list.id = :listId and m.hold is null", hints={ @QueryHint(name="org.hibernate.readOnly", value="true"), @QueryHint(name="org.hibernate.cacheable", value="true") } ), @NamedQuery( name="HeldMail", query="from Mail m where m.list.id = :listId and m.hold is not null order by m.sentDate desc", hints={ @QueryHint(name="org.hibernate.readOnly", value="true"), @QueryHint(name="org.hibernate.cacheable", value="true") } ), @NamedQuery( name="HeldMailCount", query="select count(*) from Mail m where m.list.id = :listId and m.hold is not null", hints={ @QueryHint(name="org.hibernate.cacheable", value="true") } ), @NamedQuery( name="WantsReferenceToMessageId", query="select m from Mail as m join fetch m.wantedReference as ref where ref = :messageId and m.list.id = :listId", hints={ } ), @NamedQuery( name="SoftHoldsByPerson", query="select m from Mail m, EmailAddress email where email.person.id = :personId and email.id = m.senderNormal and m.hold = 'SOFT'", hints={ } ), @NamedQuery( name="HeldMailOlderThan", query="select m from Mail m where m.hold is not null and m.arrivalDate < :cutoff", hints={ } ), @NamedQuery( name="HeldMailFrom", query="select m from Mail m where m.hold is not null and m.senderNormal = :sender and m.id <> :excluding order by m.arrivalDate desc", hints={ } ), @NamedQuery( name="CountRecentHeldMailFrom", query="select count(*) from Mail m where m.hold is not null and m.senderNormal = :sender and m.arrivalDate > :since", hints={ } ), @NamedQuery( name="RecentMailBySubject", query="select m from Mail m where m.hold is null and m.list.id = :listId and m.subject = :subject and m.arrivalDate >= :cutoff order by m.arrivalDate desc", hints={ } ), @NamedQuery( name="MailSince", query="select m from Mail m where m.hold is null and m.arrivalDate > :since", hints={ } ), @NamedQuery( name="CountMail", query="select count(*) from Mail", hints={ @QueryHint(name="org.hibernate.readOnly", value="true"), @QueryHint(name="org.hibernate.cacheable", value="true") } ) }) @Entity @Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL) @Table( appliesTo="Mail", indexes={@Index(name="mailMessageIdIndex", columnNames={"listId", "messageId"})} ) @Indexed @Log public class Mail implements Serializable, Comparable<Mail> { private static final long serialVersionUID = 1L; /** * Possible moderation states */ public enum HoldType { SOFT, // Can be flushed if msg becomes associated with someone who has Permission.POST HARD // Must be manually approved no matter what } /** */ @Id @GeneratedValue Long id; /** Nullable because content is set after object persistence */ @Column(nullable=true, length=Validator.MAX_MAIL_CONTENT) @Field @FieldBridge(impl=MessageContentBridge.class) byte[] content; /** Message id might not exist */ @Column(nullable=true, length=Validator.MAX_MAIL_MESSAGE_ID) String messageId; /** */ @Column(nullable=false, length=Validator.MAX_MAIL_SUBJECT) @Index(name="subject") @Field(boost=@Boost(5f)) String subject; /** * rfc222-style version of the sender. This is pretty, includes the * person's name. Note that it is not usually the envelope sender; it will * be the Sender: field if it exists, the first From: element if it exists, * and last of all the envelope sender. * * Don't try to use hibernate email validator, it doesn't understand * the rfc222 style with personal names. */ @Column(nullable=true, length=Validator.MAX_MAIL_SENDER) String sender; /** * This field is the normalized interpretation of the sender. Mostly this gets * used to match for auto self moderation. */ // @Email // The validator failed on this address: SRS0=aHFE=YF=pobox.com=fredx@bounce2.pobox.com // It looks valid to me, so I can only guess that the validation pattern is broken. @Column(nullable=false, length=Validator.MAX_MAIL_SENDER) @Index(name="senderIndex") String senderNormal; /** Date the entity was created, not from header fields */ @Column(nullable=false) @Index(name="arrivalDateIdx") Date arrivalDate; /** Date from the header fields, or the arrivalDate if there was no header */ @Column(nullable=false) @Index(name="sentDateIdx") Date sentDate; /** */ @ManyToOne @JoinColumn(name="listId", nullable=false) @Field @FieldBridge(impl=MailingListBridge.class) MailingList list; /** */ @ManyToOne @JoinColumn(name="parentId", nullable=true) Mail parent; /** */ @OneToMany(cascade=CascadeType.ALL, mappedBy="parent") @Sort(type=SortType.NATURAL) @Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL) SortedSet<Mail> replies; /** */ @OneToMany(cascade=CascadeType.ALL, mappedBy="mail") // Disabled caching until instrumentation problem with jboss-4.0.4.GA resolved //@Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL) Set<Attachment> attachments; /** * This represents a list of references we are more interested * in using as our parent than what we have currently. The first * entry is the best possible. */ @ElementCollection @JoinTable(name="WantedReference", joinColumns={@JoinColumn(name="mailId")}) @Column(name="messageId", nullable=false) @IndexColumn(name="ord") @Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL) //@Index(name="mailWantedRefIndex") // TODO: this doesn't seem to work List<String> wantedReference; /** * Is it held for moderation? If null, no need for moderation. */ @Enumerated(EnumType.STRING) @Column(nullable=true) @Index(name="holdIndex") HoldType hold; /** */ public Mail() {} /** * Pulls the date out of the message. */ public Mail(String envelopeSender, SubEthaMessage msg, MailingList list, HoldType holdFor) throws MessagingException { this(envelopeSender, msg, list, holdFor, msg.getSentDate()); } /** * Creates a new Mail object. DOES NOT SET THE CONTENT. * * Content must be set separately because the detacher requires the mail object * to exist before it can create Attachment objects, and the assigned content * must contain the newly created ids of the attachments. * * @param envelopeSender can be null if no envelope sender was specified * @param holdFor can be null which means none required * @param sentDate is the date which should be used as the sent date. If null, current time is chosen. */ public Mail(String envelopeSender, SubEthaMessage msg, MailingList list, HoldType holdFor, Date sentDate) throws MessagingException { log.log(Level.FINE,"Creating new mail"); this.arrivalDate = new Timestamp(System.currentTimeMillis()); this.sentDate = sentDate; if (this.sentDate == null) this.sentDate = this.arrivalDate; this.list = list; this.hold = holdFor; this.subject = msg.getSubject(); if (this.subject == null) this.subject = ""; this.messageId = msg.getMessageID(); // Convoluted process to determine sender. // Check, in order: Sender field, first entry of From field, envelope sender InternetAddress senderField = msg.getSenderWithFallback(envelopeSender); if (senderField == null) this.setSender(""); else this.setSender(senderField.toString()); this.replies = new TreeSet<Mail>(); this.attachments = new HashSet<Attachment>(); } /** */ public Long getId() { return this.id; } /** */ public byte[] getContent() { return this.content; } /** */ public void setContent(byte[] value) { this.content = value; } /** * Convenience method */ public void setContent(SubEthaMessage msg) throws MessagingException { // TODO: optimize this to do less memory copying byte[] raw; try { ByteArrayOutputStream tmpStream = new ByteArrayOutputStream(8192); msg.writeTo(tmpStream); raw = tmpStream.toByteArray(); } catch (IOException ex) { throw new EJBException(ex); } this.setContent(raw); } /** */ public String getMessageId() { return this.messageId; } public void setMessageId(String value) { this.messageId = value; } /** */ public String getSubject() { return this.subject; } public void setSubject(String value) { log.log(Level.FINE,"Setting subject of {0} to {1}", new Object[]{this, value}); this.subject = value; } /** * @return a single rfc222-compilant address. It will * be parseable with InternetAddress.parse(). This is * just a pretty alias for getSender(). */ public String getFrom() { return this.sender; } /** * Convenient way of getting the javamail object. */ public InternetAddress getFromAddress() { try { return new InternetAddress(this.getFrom()); } catch (AddressException ex) { // Should be impossible because we were created with a valid InternetAddress throw new RuntimeException(ex); } } /** * @return the rfc822 version of our understanding of who sent the message */ public String getSender() { return this.sender; } /** * @return the normalized (just box@domain.tld) version of the sender * * @see Validator#normalizeEmail(String) */ public String getSenderNormal() { return this.senderNormal; } /** * @param value is the rfc822 value of who we understand is the sender of this message. * @throws AddressException if the value is a bad address */ public void setSender(String value) throws AddressException { log.log(Level.FINE,"Setting sender of {0} to {1}", new Object[]{this, value}); this.sender = value; InternetAddress addy = new InternetAddress(value); this.senderNormal = Validator.normalizeEmail(addy.getAddress()); } /** This is the date the mail came into the system */ public Date getArrivalDate() { return this.arrivalDate; } /** This is the "natural" date of the mail, from the header */ public Date getSentDate() { return this.sentDate; } public void setSentDate(Date value) { this.sentDate = value; } /** */ public HoldType getHold() { return this.hold; } /** * Releases a moderation hold */ public void approve() { this.hold = null; } /** */ public Mail getParent() { return this.parent; } public void setParent(Mail value) { if (value.getId().equals(this.getId())) throw new IllegalArgumentException("Parent cannot be self!"); this.parent = value; } /** */ public Set<Mail> getReplies() { return this.replies; } /** * Gets every message related to this one, or any reply. * @return A list of the children, their children and so forth. */ public List<Mail> getDescendents() { Set<Mail> children = this.getReplies(); if (children == null || children.size() < 1) return null; List<Mail> descendents = new Vector<Mail>(children.size()); for (Object element : children) { Mail child = (Mail) element; List<Mail> underlings = child.getDescendents(); if(underlings != null && underlings.size() > 0) descendents.addAll(underlings); } return descendents; } /** */ @Override public String toString() { return this.getClass() + " {id=" + this.id + ", subject=" + this.subject + "}"; } /** */ public MailingList getList() { return this.list; } /** */ public List<String> getWantedReference() { return this.wantedReference; } public void setWantedReference(List<String> value) { this.wantedReference = value; } /** */ public Set<Attachment> getAttachments() { return this.attachments; } /** * Natural sort order is based on sent date, but we need * to make sure that we don't return equal if we aren't. */ public int compareTo(Mail other) { int result = TimeUtils.compareDates(other.getSentDate(), this.sentDate); if (result == 0) return other.id.compareTo(this.id); else return result; } }