package com.juick.server.api; import com.juick.Message; import com.juick.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.HttpForbiddenException; import com.juick.service.EmailService; import com.juick.service.MessagesService; import com.juick.service.UserService; 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.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 EmailManager emailManager; @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); String signupUrl = String.format("Follow this link to create Juick account: https://juick.com/signup?type=email&hash=%s", verificationCode); emailManager.sendEmail("noreply@juick.com", from, "Juick registration", signupUrl, StringUtils.EMPTY, Collections.emptyMap()); } } } else { throw new HttpForbiddenException(); } } @ApiIgnore @PostMapping("/api/new_event") @ResponseStatus(value = HttpStatus.OK) public void processEvent(@Visitor User current, InputStream data) throws Exception { if (current.getName().equals(serviceUser)) { MimeMessage msg = new MimeMessage(session, data); 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); } catch (IOException e) { logger.info("attachment error", e); } }); } logger.info("got text event: {}", body[0]); } 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); } }