Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Magic attachments #1466

Merged
merged 12 commits into from
Feb 10, 2025
4 changes: 4 additions & 0 deletions tmail-backend/apps/distributed/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,10 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,18 @@
import org.apache.james.jmap.InjectionKeys;
import org.apache.james.jmap.JMAPListenerModule;
import org.apache.james.jmap.JMAPModule;
import org.apache.james.jmap.rfc8621.RFC8621MethodsModule;
import org.apache.james.json.DTO;
import org.apache.james.json.DTOModule;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.cassandra.CassandraMailboxManager;
import org.apache.james.mailbox.cassandra.mail.CassandraAttachmentMapper;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mailbox.model.MultimailboxesSearchQuery;
import org.apache.james.mailbox.searchhighligt.SearchHighlighter;
import org.apache.james.mailbox.searchhighligt.SearchSnippet;
import org.apache.james.mailbox.store.mail.model.impl.MessageParser;
import org.apache.james.mailbox.store.search.ListeningMessageSearchIndex;
import org.apache.james.mailbox.store.search.MessageSearchIndex;
import org.apache.james.mailbox.store.search.SimpleMessageSearchIndex;
Expand Down Expand Up @@ -191,6 +194,8 @@
import com.linagora.tmail.james.jmap.method.MessageVaultCapabilitiesModule;
import com.linagora.tmail.james.jmap.module.OSContactAutoCompleteModule;
import com.linagora.tmail.james.jmap.oidc.WebFingerModule;
import com.linagora.tmail.james.jmap.perfs.TMailCleverBlobResolverModule;
import com.linagora.tmail.james.jmap.perfs.TMailCleverMessageParser;
import com.linagora.tmail.james.jmap.publicAsset.CassandraPublicAssetRepositoryModule;
import com.linagora.tmail.james.jmap.publicAsset.PublicAssetsModule;
import com.linagora.tmail.james.jmap.service.discovery.LinagoraServicesDiscoveryModule;
Expand Down Expand Up @@ -270,7 +275,9 @@ protected void configure() {
new ForwardGetMethodModule(),
new ForwardSetMethodModule(),
new JMAPServerModule(),
JMAPModule.INSTANCE,
new JMAPModule(),
new RFC8621MethodsModule(),
new TMailCleverBlobResolverModule(),
new JmapEventBusModule(),
new PublicAssetsModule(),
new KeystoreCassandraModule(),
Expand Down Expand Up @@ -316,7 +323,11 @@ protected void configure() {

public static final Module CASSANDRA_MAILBOX_MODULE = Modules.combine(
new CassandraConsistencyTaskSerializationModule(),
new CassandraMailboxModule(),
Modules.override(new CassandraMailboxModule())
.with(binder -> {
binder.bind(CassandraAttachmentMapper.AttachmentIdAssignationStrategy.class).to(TMailCleverAttachmentIdAssignationStrategy.class);
binder.bind(MessageParser.class).to(TMailCleverMessageParser.class);
}),
new MailboxModule(),
new TikaMailboxModule());

Expand Down Expand Up @@ -354,7 +365,7 @@ protected void configure() {
new TeamMailboxModule(),
new TMailMailboxSortOrderProviderModule(),
new TmailEventModule(),
new TmailEventDeadLettersModule(),
new TMailEventDeadLettersModule(),
new TMailIMAPModule());

public static void main(String[] args) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/********************************************************************
* As a subpart of Twake Mail, this file is edited by Linagora. *
* *
* https://twake-mail.com/ *
* https://linagora.com *
* *
* This file is subject to The Affero Gnu Public License *
* version 3. *
* *
* https://www.gnu.org/licenses/agpl-3.0.en.html *
* *
* This program is distributed in the hope that it will be *
* useful, but WITHOUT ANY WARRANTY; without even the implied *
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR *
* PURPOSE. See the GNU Affero General Public License for *
* more details. *
********************************************************************/
package com.linagora.tmail.james.app;

import org.apache.james.mailbox.cassandra.mail.CassandraAttachmentMapper;
import org.apache.james.mailbox.model.AttachmentId;
import org.apache.james.mailbox.model.MessageId;
import org.apache.james.mailbox.model.ParsedAttachment;
import org.apache.james.mailbox.model.StringBackedAttachmentId;

import com.linagora.tmail.james.jmap.perfs.TMailCleverParsedAttachment;

public class TMailCleverAttachmentIdAssignationStrategy implements CassandraAttachmentMapper.AttachmentIdAssignationStrategy {
@Override
public AttachmentId assign(ParsedAttachment parsedAttachment, MessageId messageId) {
if (parsedAttachment instanceof TMailCleverParsedAttachment TMailCleverParsedAttachment) {
return StringBackedAttachmentId.from(TMailCleverParsedAttachment.translate(messageId));
}
return StringBackedAttachmentId.random();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import com.google.inject.Singleton;
import com.linagora.tmail.event.TmailEventSerializer;

public class TmailEventDeadLettersModule extends AbstractModule {
public class TMailEventDeadLettersModule extends AbstractModule {

@Provides
@Singleton
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/********************************************************************
* As a subpart of Twake Mail, this file is edited by Linagora. *
* *
* https://twake-mail.com/ *
* https://linagora.com *
* *
* This file is subject to The Affero Gnu Public License *
* version 3. *
* *
* https://www.gnu.org/licenses/agpl-3.0.en.html *
* *
* This program is distributed in the hope that it will be *
* useful, but WITHOUT ANY WARRANTY; without even the implied *
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR *
* PURPOSE. See the GNU Affero General Public License for *
* more details. *
********************************************************************/

package com.linagora.tmail.james.jmap.perfs;

import org.apache.james.jmap.routes.BlobResolver;
import org.apache.james.jmap.routes.MessageBlobResolver;
import org.apache.james.jmap.routes.UploadResolver;

import com.google.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;

public class TMailCleverBlobResolverModule extends AbstractModule {
@Override
protected void configure() {
Multibinder<BlobResolver> blobResolverMultibinder = Multibinder.newSetBinder(binder(), BlobResolver.class);
blobResolverMultibinder.addBinding().to(MessageBlobResolver.class);
blobResolverMultibinder.addBinding().to(UploadResolver.class);
blobResolverMultibinder.addBinding().to(TMailCleverBlobResolver.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** ******************************************************************
* As a subpart of Twake Mail, this file is edited by Linagora. *
* *
* https://twake-mail.com/ *
* https://linagora.com *
* *
* This file is subject to The Affero Gnu Public License *
* version 3. *
* *
* https://www.gnu.org/licenses/agpl-3.0.en.html *
* *
* This program is distributed in the hope that it will be *
* useful, but WITHOUT ANY WARRANTY; without even the implied *
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR *
* PURPOSE. See the GNU Affero General Public License for *
* more details. *
* ****************************************************************** */

package com.linagora.tmail.james.jmap.perfs

import jakarta.inject.Inject
import org.apache.james.jmap.mail.BlobId
import org.apache.james.jmap.routes.{AttachmentBlobResolver, BlobResolutionResult, BlobResolver, MessagePartBlobResolver, NonApplicable}
import org.apache.james.mailbox.MailboxSession
import reactor.core.scala.publisher.SMono

class TMailCleverBlobResolver @Inject() (messagePartBlobResolver: MessagePartBlobResolver, attachmentBlobResolver: AttachmentBlobResolver) extends BlobResolver {
override def resolve(blobId: BlobId, mailboxSession: MailboxSession): SMono[BlobResolutionResult] =
attachmentBlobResolver.resolve(blobId, mailboxSession)
.flatMap[BlobResolutionResult] {
case NonApplicable => messagePartBlobResolver.resolve(blobId, mailboxSession)
case any => SMono.just(any)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/** ******************************************************************
* As a subpart of Twake Mail, this file is edited by Linagora. *
* *
* https://twake-mail.com/ *
* https://linagora.com *
* *
* This file is subject to The Affero Gnu Public License *
* version 3. *
* *
* https://www.gnu.org/licenses/agpl-3.0.en.html *
* *
* This program is distributed in the hope that it will be *
* useful, but WITHOUT ANY WARRANTY; without even the implied *
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR *
* PURPOSE. See the GNU Affero General Public License for *
* more details. *
* ****************************************************************** */

package com.linagora.tmail.james.jmap.perfs

import java.io.InputStream
import java.util

import com.google.common.io.ByteSource
import jakarta.inject.Inject
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream
import org.apache.james.jmap.mail.{BlobId, Disposition, EmailBodyPart}
import org.apache.james.jmap.method.ZoneIdProvider
import org.apache.james.mailbox.model.{Cid, MessageId, ParsedAttachment}
import org.apache.james.mailbox.store.mail.model.impl.{FileBufferedBodyFactory, MessageParser}
import org.apache.james.mime4j.codec.DecodeMonitor
import org.apache.james.mime4j.dom.{Message, SingleBody}
import org.apache.james.mime4j.message.{DefaultMessageBuilder, DefaultMessageWriter}
import org.apache.james.mime4j.stream.MimeConfig

import scala.jdk.CollectionConverters._
import scala.jdk.OptionConverters._


case class EmailBodyPartContent(part: EmailBodyPart) extends ByteSource {
override def size: Long = part.size.value

override def openStream: InputStream = part.entity.getBody match {
case body: SingleBody => body.getInputStream
case body =>
val writer = new DefaultMessageWriter
val outputStream = new UnsynchronizedByteArrayOutputStream()
writer.writeBody(body, outputStream)
outputStream.toInputStream
}
}

case class TMailCleverParsedAttachment(parsedAttachment: ParsedAttachment, blobId: BlobId) extends ParsedAttachment(parsedAttachment.getContentType,
parsedAttachment.getContent, parsedAttachment.getName, parsedAttachment.getCid, parsedAttachment.isInline) {

def translate(messageId: MessageId): String = {
val rawValue = blobId.value.value
if (rawValue.startsWith(TMailCleverParsedAttachment.placeholder)) {
messageId.serialize() + rawValue.substring(TMailCleverParsedAttachment.placeholder.length)
} else {
throw new RuntimeException("unsuported blobid value " + rawValue)
}
}
}

object TMailCleverParsedAttachment {
val placeholder: String = "___PLACEHOLDER___"
}

class TMailCleverMessageParser @Inject() (zoneIdProvider: ZoneIdProvider) extends MessageParser {
override def retrieveAttachments(fullContent: InputStream): MessageParser.ParsingResult = {
val defaultMessageBuilder = new DefaultMessageBuilder
defaultMessageBuilder.setMimeEntityConfig(MimeConfig.PERMISSIVE)
defaultMessageBuilder.setDecodeMonitor(DecodeMonitor.SILENT)
val bodyFactory = new FileBufferedBodyFactory
defaultMessageBuilder.setBodyFactory(bodyFactory)
try {
val message = defaultMessageBuilder.parseMessage(fullContent)
new MessageParser.ParsingResult(retrieveAttachments(message), () => bodyFactory.dispose())
} catch {
case e: Exception =>
// Release associated temporary files
bodyFactory.dispose()
throw e
}
}

override def retrieveAttachments(message: Message): util.List[ParsedAttachment] = {
val value = BlobId.of(TMailCleverParsedAttachment.placeholder).get
val triedPart = EmailBodyPart.of(None, zoneIdProvider.get(), value, message)
triedPart.map(part => part.attachments).toOption.getOrElse(List())
.map(attachment => {
val isInline = attachment.disposition.contains(Disposition.INLINE) && attachment.cid.isDefined
TMailCleverParsedAttachment(ParsedAttachment.builder
.contentType(attachment.`type`.value)
.content(EmailBodyPartContent(attachment))
.name(attachment.name.map(_.value).toJava)
.cid(attachment.cid.map(_.getValue).map(Cid.from).toJava)
.inline(isInline), attachment.blobId.get)
.asInstanceOf[ParsedAttachment]
}).asJava
}
}