From 7a2f89266c8f6337e4e81a2fd8488e0f80f4f9bd Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Fri, 3 Apr 2020 23:53:23 +0300 Subject: Reorganize layout and code cleanup --- src/main/java/com/juick/www/api/Service.java | 223 +++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 src/main/java/com/juick/www/api/Service.java (limited to 'src/main/java/com/juick/www/api/Service.java') diff --git a/src/main/java/com/juick/www/api/Service.java b/src/main/java/com/juick/www/api/Service.java new file mode 100644 index 00000000..cb918682 --- /dev/null +++ b/src/main/java/com/juick/www/api/Service.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2008-2019, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import com.juick.model.Message; +import com.juick.model.User; +import com.juick.model.CommandResult; +import com.juick.server.CommandsManager; +import com.juick.server.EmailManager; +import com.juick.server.ServerManager; +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.HttpForbiddenException; +import com.juick.service.EmailService; +import com.juick.service.MessagesService; +import com.juick.service.UserService; +import com.juick.service.component.AccountVerificationEvent; +import com.juick.service.security.annotation.Visitor; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.mail.util.MimeMessageParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.async.AsyncRequestTimeoutException; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import springfox.documentation.annotations.ApiIgnore; + +import javax.inject.Inject; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.*; + +@Controller +public class Service { + private static Logger logger = LoggerFactory.getLogger("Session"); + @Inject + private UserService userService; + @Inject + private EmailService emailService; + @Inject + private MessagesService messagesService; + @Inject + private CommandsManager commandsManager; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + @Value("${api_user:juick}") + private String serviceUser; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + @Value("${banned_emails:}") + private String[] ignoredEmails; + @Inject + private ServerManager serverManager; + + private Session session = Session.getDefaultInstance(new Properties()); + + @ApiIgnore + @PostMapping("/api/mail") + @ResponseStatus(value = HttpStatus.OK) + public void processMail(@Visitor User current, InputStream data) throws Exception { + if (current.getName().equals(serviceUser)) { + MimeMessage msg = new MimeMessage(session, data); + String[] returnPaths = msg.getHeader("Return-Path"); + if (returnPaths != null) { + logger.info("got msg with return path {}", returnPaths[0]); + if (returnPaths[0].equals("<>")) { + return; + } + } + String from = msg.getFrom() == null || msg.getFrom().length > 1 ? ((InternetAddress) msg.getSender()).getAddress() + : ((InternetAddress) msg.getFrom()[0]).getAddress(); + + User visitor = userService.getUserByEmail(from); + if (!visitor.isAnonymous()) { + MimeMessageParser parser = new MimeMessageParser(msg); + parser.parse(); + final String[] body = {parser.getPlainContent()}; + if (body[0] == null) { + parser.getAttachmentList().stream() + .filter(a -> a.getContentType().equals("text/plain")).findFirst() + .ifPresent(a -> { + try { + body[0] = IOUtils.toString(a.getInputStream(), StandardCharsets.UTF_8); + logger.info("got text: {}", body[0]); + } catch (IOException e) { + logger.info("attachment error", e); + } + }); + } + final String[] attachmentFName = new String[1]; + parser.getAttachmentList().stream().filter(a -> + a.getContentType().equals("image/jpeg") || a.getContentType().equals("image/png")) + .findFirst().ifPresent(a -> { + logger.info("got attachment: {}", a.getContentType()); + String attachmentType; + if (a.getContentType().equals("image/jpeg")) { + attachmentType = "jpg"; + } else { + attachmentType = "png"; + } + attachmentFName[0] = DigestUtils.md5Hex(UUID.randomUUID().toString()) + "." + attachmentType; + try { + logger.info("got inputstream: {}", a.getInputStream()); + FileOutputStream fos = new FileOutputStream(Paths.get(tmpDir, attachmentFName[0]).toString()); + IOUtils.copy(a.getInputStream(), fos); + fos.close(); + } catch (IOException e) { + logger.info("attachment error", e); + } + }); + String[] inReplyToHeaders = msg.getHeader("In-Reply-To"); + if (inReplyToHeaders != null && inReplyToHeaders.length > 0) { + int mid, rid; + var originalMessage = messagesService.findMessageByProperty("messageId", inReplyToHeaders[0]); + if (originalMessage.isPresent()) { + mid = originalMessage.get().getLeft(); + rid = originalMessage.get().getRight(); + } else { + Scanner inReplyToScanner = new Scanner(inReplyToHeaders[0].trim()).useDelimiter(EmailManager.MSGID_PATTERN); + mid = Integer.parseInt(inReplyToScanner.next()); + rid = Integer.parseInt(inReplyToScanner.next()); + } + logger.info("Message is reply to #{}/{}", mid, rid); + body[0] = rid > 0 ? String.format("#%d/%d %s", mid, rid, body[0]) + : String.format("#%d %s", mid, body[0]); + } + URI attachmentUri = StringUtils.isNotEmpty(attachmentFName[0]) ? URI.create(String.format("juick://%s", attachmentFName[0])) + : URI.create(StringUtils.EMPTY); + CommandResult result = commandsManager.processCommand(visitor, body[0], attachmentUri); + if (result.getNewMessage().isPresent()) { + String[] messageIds = msg.getHeader("Message-Id"); + if (messageIds.length == 1) { + Message message = result.getNewMessage().get(); + messagesService.setMessageProperty(message.getMid(), message.getRid(), "messageId", messageIds[0]); + } else { + logger.warn("Wrong number of Message-Id headers"); + } + } + } else { + if (!Arrays.asList(ignoredEmails).contains(from)) { + String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); + emailService.addVerificationCode(null, from, verificationCode); + applicationEventPublisher.publishEvent(new AccountVerificationEvent(this, from, verificationCode)); + } + } + } else { + throw new HttpForbiddenException(); + } + } + @ApiIgnore + @PostMapping("/api/mail/unsubscribe") + @ResponseStatus(value = HttpStatus.OK) + public void processMailUnsubscribe(@Visitor User current, InputStream data) throws Exception { + if (current.getName().equals(serviceUser)) { + MimeMessage msg = new MimeMessage(session, data); + String from = msg.getFrom() == null || msg.getFrom().length > 1 ? ((InternetAddress) msg.getSender()).getAddress() + : ((InternetAddress) msg.getFrom()[0]).getAddress(); + + User visitor = userService.getUserByEmail(from); + if (!visitor.isAnonymous()) { + if (!emailService.disableEmail(visitor, from)) { + throw new HttpBadRequestException(); + } + } + } else { + throw new HttpForbiddenException(); + } + } + private void endSession(SseEmitter emitter) { + serverManager.getSessions().stream() + .filter(s -> s.getEmitter().equals(emitter)) + .forEach(session -> serverManager.getSessions().remove(session)); + } + @GetMapping("/api/events") + public SseEmitter handle(@Visitor User visitor) throws IOException { + logger.info("{} connected", visitor.getName()); + if (!visitor.isAnonymous()) { + userService.updateLastSeen(visitor); + } + SseEmitter emitter = new SseEmitter(600000L); + serverManager.getSessions().add(new ServerManager.EventSession(visitor, emitter)); + + emitter.onCompletion(() -> endSession(emitter)); + emitter.onTimeout(() -> endSession(emitter)); + + return emitter; + } + @ExceptionHandler(AsyncRequestTimeoutException.class) + public void eventErrorHandler(Exception ex) { + logger.debug("SSE timeout", ex); + } +} -- cgit v1.2.3