Search code examples
xpagesattachmentmime

How to Add and Remove MIME attachments correctly


I am adding MIME attachments to a document like this

try{
    var d = database.getView("Main").getFirstDocument()
    var it = d.getFirstItem("Body")
    var att:NotesEmbeddedObject = it.getEmbeddedObject("mydoc.docx")
    var streamDOC:NotesStream = session.createStream()

    streamDOC.setContents(att.getInputStream())


    var newd;
    newd = database.getView("NewD").getFirstDocument()
    if(newd==null){
        newd = database.createDocument()
        newd.replaceItemValue("Form","Main")
        var me = newd.createMIMEEntity("Body")
    }else{
        var me = newd.getMIMEEntity("Body") 
    }

    var filename = "test.pdf"
    var mc = me.createChildEntity();
    var he = mc.createHeader("Content-Disposition")
    he.setHeaderVal("attachment; filename=\"" + filename + "\"");
    he = mc.createHeader("Content-ID");
    he.setHeaderVal( "<" + filename + ">" );
    mc.setContentFromBytes(streamDOC, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", NotesMIMEEntity.ENC_IDENTITY_8BIT);
    newd.save()
    print("success")
}catch(e){
    print("fail " + e)
}

and in a repeat I provide a delete button

var eo = nd.getDocument().getAttachment(att)
eo.remove()
nd.save()

the attachment are removed from the document, in Ytria I can see that the $FILE items are removed but not the BODY items. the problem with this is that if I add a new attachment to the same document all the attachments I removed previously come back

This is how the document looks before removing the attachments.

The file size here is unfortnuately 0Kb because I used the wrong screenshot. from the beginnin all $File items have correct size

enter image description here

This is how the document look after I removed the attachments (using the script above)

enter image description here

This is what the document look like after I add one attachment (using the script above) after I removed them

enter image description here

  • Could I be doing something wrong when adding or removing the attachments? (see script)
  • It does not seem to matter if the Body field has the "store content as MIME" option set or not
  • see also this question How to Add and Remove attachments using MIME

Solution

  • If you work with MIME methods to attach the file why not work with MIME methods to remove it as well?

    I use my own framework so the following code might give you the impression to overcomplicate things but hopefully you should get the gist of it:

    I have an enum that helps me navigate through the various MIME types. In this case you are dealing with ATTACHMENT:

    public enum MimeContentType {
    
        ATTACHMENT("attachment") {
    
            @Override
            public boolean matches(String[] headers) {
                int score = 0;
    
                for (String header : headers) {
                    if (header.startsWith("Content-Disposition")) {
                        score++;
                    }
    
                    if (header.contains("attachment")) {
                        score++;
                    }
    
                    if (header.contains("filename")) {
                        score++;
                    }
    
                    if (score == 3) {
                        return true;
                    }
                }
    
                return false;
            }
    
        },
        TEXT("text"),
        TEXT_HTML("text/html"),
        TEXT_PLAIN("text/plain");
    
        private final String type;
    
        private MimeContentType(String type) {
            this.type = type;
        }
    
        public boolean matches(String[] headers) {
            for (String header : headers) {
                if (header.startsWith("Content-Type") && header.contains(type)) {
                    return true;
                }
            }
    
            return false;
        }
    
    }
    

    Then some helper classes:

    @FunctionalInterface
    public interface ThrowableConsumer<T> extends Consumer<T> {
    
        @Override
        default void accept(final T t) {
            try {
                acceptOrThrow(t);
            } catch (final Throwable e) {
                throw new RuntimeException(e);
            }
        }
    
        void acceptOrThrow(T t) throws Throwable;
    
    }
    
    @FunctionalInterface
    public interface ThrowableFunction<T, R> extends Function<T, R> {
    
        @Override
        default R apply(T t) {
            try {
                return applyOrThrow(t);
            } catch (final Throwable e) {
                throw new RuntimeException(e);
            }
        }
    
        R applyOrThrow(T t) throws Throwable;
    
    }
    
    @FunctionalInterface
    public interface ThrowablePredicate<T> extends Predicate<T> {
    
        @Override
        default boolean test(T t) {
            try {
                return testOrThrow(t);
            } catch (final Throwable e) {
                throw new RuntimeException(e);
            }
        }
    
        boolean testOrThrow(T t) throws Throwable;
    
    }
    
    @FunctionalInterface
    public interface ThrowableSupplier<T> extends Supplier<T> {
    
        @Override
        default T get() {
            try {
                return getOrThrow();
            } catch (final Throwable e) {
                throw new RuntimeException(e);
            }
        }
    
        T getOrThrow() throws Throwable;
    
    }
    
    public enum DominoUtil {
        ;
    
        private static final Vector<String> MIME_FILTERED_HEADERS = new Vector<>();
    
        static {
            MIME_FILTERED_HEADERS.add("Content-Type");
            MIME_FILTERED_HEADERS.add("Content-Disposition");
        }
    
        public static List<MIMEEntity> getMimeEntitiesByContentType(MIMEEntity entity,
                MimeContentType contentType) throws NotesException {
            Objects.requireNonNull(entity, "Entity cannot be null");
            Objects.requireNonNull(contentType, "Content type cannot be null");
    
            List<MIMEEntity> subentities = new ArrayList<>();
            MIMEEntity nextEntity = null;
    
            try {
                nextEntity = entity.getNextEntity();
    
                while (nextEntity != null) {
                    String[] entityFilteredHeaders = nextEntity
                            .getSomeHeaders(MIME_FILTERED_HEADERS, true)
                            .split("\\n");
    
                    if (contentType.matches(entityFilteredHeaders)) {
                        subentities.add(nextEntity);
                    }
    
                    nextEntity = nextEntity.getNextEntity();
                }
            } finally {
                DominoUtil.recycle(nextEntity);
            }
    
            return subentities;
        }
    
        public final static MIMEEntity getMimeEntity(Document doc, String itemName,
                boolean createOnFail) throws NotesException {
            if (itemName == null) {
                throw new NullPointerException("Invalid MIME entity item name");
            }
    
            MIMEEntity mimeEntity = doc.getMIMEEntity(itemName);
    
            if (mimeEntity == null) {
                if (doc.hasItem(itemName)) {
                    doc.removeItem(itemName);
                }
    
                if (createOnFail) {
                    mimeEntity = doc.createMIMEEntity(itemName);
                }
            }
    
            return mimeEntity;
        }
    
        public static Optional<String> getMimeEntityAttachmentFilename(MIMEEntity entity) throws NotesException {
            Objects.requireNonNull(entity, "Entity cannot be null");
    
            return getMimeEntityHeaderValAndParams(
                    entity, (ThrowablePredicate<MIMEHeader>) h -> h.getHeaderVal().equals("attachment"))
                            .map(s -> {
                                Matcher m = Pattern.compile("filename=['\"]?([^'\"\\s]+)").matcher(s);
                                m.find();
                                return m.group(1);
                            });
        }
    
        public static Optional<String> getMimeEntityHeaderValAndParams(
                MIMEEntity entity, Predicate<MIMEHeader> matcher) throws NotesException {
            Objects.requireNonNull(entity, "Entity cannot be null");
            Objects.requireNonNull(matcher, "Matcher cannot be null");
    
            Vector<?> headers = entity.getHeaderObjects();
    
            try {
                return headers
                        .stream()
                        .map(MIMEHeader.class::cast)
                        .filter(matcher)
                        .map((ThrowableFunction<MIMEHeader, String>) MIMEHeader::getHeaderValAndParams)
                        .findFirst();
            } finally {
                recycle(headers);
            }
        }
    
        public static void recycle(Base... bases) {
            for (Base base : bases) {
                if (base != null) {
                    try {
                        base.recycle();
                    } catch (Exception e) {
                        // Do nothing
                    }
                }
            }
        }
    
        public static void recycle(Collection<? extends Object> objs) {
            objs.stream()
                    .filter(o -> o instanceof Base)
                    .map(o -> (Base) o)
                    .forEach(DominoUtil::recycle);
        }
    
    }
    

    Finally the method that would do the job:

    public class Example {
    
        public static void yourEntryPoint() {
            try {
                // The last param is just a way to create an attachment from text
                // You have InputStream to pass along obviously
                addAttachment(doc, "Body", "fake1.txt", "this is fake text1");
                addAttachment(doc, "Body", "fake2.txt", "this is fake text2");
                addAttachment(doc, "Body", "fake3.txt", "this is fake text3");
                removeAttachment(doc, "Body", "fake2.txt");
                removeAttachment(doc, "Body", "fake3.txt");
    
            } catch (NotesException e) {
                throw new RuntimeException(e);
            }
        }
    
        private static void addAttachment(Document doc, String itemName, String fileName, String data)
                throws NotesException {
            MIMEEntity mimeEntity = null;
            Stream stm = null;
    
            try {
                mimeEntity = DominoUtil.getMimeEntity(doc, itemName, true);
    
                Optional<MIMEEntity> optAttEntity = getAttachmentMimeEntity(mimeEntity, fileName);
    
                MIMEEntity attachmentEntity = null;
    
                if (optAttEntity.isPresent()) {
                    attachmentEntity = optAttEntity.get();
                } else {
                    attachmentEntity = mimeEntity.createChildEntity();
                    MIMEHeader header = attachmentEntity.createHeader("Content-Disposition");
                    header.setHeaderValAndParams("attachment; filename=\"" + fileName + "\"");
                }
    
                stm = doc.getParentDatabase().getParent().createStream();
                stm.writeText(data);
    
                attachmentEntity.setContentFromBytes(stm,
                        "application/octet-stream",
                        MIMEEntity.ENC_IDENTITY_BINARY);
    
                stm.close();
    
                doc.closeMIMEEntities(true, itemName);
            } finally {
                DominoUtil.recycle(stm);
                DominoUtil.recycle(mimeEntity);
            }
        }
    
        private static void removeAttachment(Document doc, String itemName, String fileName)
                throws NotesException {
            MIMEEntity mimeEntity = null;
    
            try {
                // Get MIME entity
                mimeEntity = DominoUtil.getMimeEntity(doc, itemName, true);
    
                Optional<MIMEEntity> optAttEntity = getAttachmentMimeEntity(mimeEntity, fileName);
    
                if (!optAttEntity.isPresent()) {
                    return;
                }
    
                optAttEntity.get().remove();
    
                // Header cleaning on empty entity
                if (mimeEntity.getFirstChildEntity() != null) {
                    doc.closeMIMEEntities(true, itemName);
                } else {
                    mimeEntity.remove();
                }
            } finally {
                DominoUtil.recycle(mimeEntity);
            }
        }
    
        private static Optional<MIMEEntity> getAttachmentMimeEntity(MIMEEntity root, String fileName)
                throws NotesException {
            return DominoUtil
                    .getMimeEntitiesByContentType(root, MimeContentType.ATTACHMENT)
                    .stream()
                    .filter((ThrowablePredicate<MIMEEntity>) mime -> {
                        Optional<String> opt = DominoUtil.getMimeEntityAttachmentFilename(mime);
    
                        return opt.isPresent() && opt.get().equals(fileName);
                    })
                    .findFirst();
        }
    
    }