이메일 재작업: test rake 작업 출력

최근 이메일 작업(emails:task) 및 관련 코드를 검토하여 각 실패 코드 경로(failure codepath)를 테스트하고 오류 메시지를 다듬는 작업을 진행했습니다.

또한 DISCOURSE_SMTP_ENABLE_START_TLS=false를 설정했을 때의 효과가 (거의) 없음을 발견했습니다. 이 설정을 해도 STARTTLS가 실제로 비활성화되지 않았으며, 실제로는 연결 시 TLS(DISCOURSE_SMTP_FORCE_TLS=true)와 함께 문제없이 공존할 수 있었습니다.

따라서 다음과 같이 수정했습니다:

이 PR을 병합하기 전에, DISCOURSE_SMTP_ENABLE_STARTTLS=false가 설정된 경우 대시보드에 관리자 경고를 표시하는 것이 적절하다고 생각합니다. 이 설정을 적용했지만 실제로는 불필요했으며, 여전히 STARTTLS에 의존하고 있는 셀프호스터(self-hoster)가 적어도 한 명은 있을 것으로 추정됩니다.

좋은 작업이네요! 제가 (아마도) 알아챈 점이 하나 있는데, rake 태스크가 실제 전송 시 사용되는 코드(예: /admin/email 테스트 페이지에서)와 동일한 코드를 사용하지 않는다는 것입니다. UX에서는 동작했지만 rake 태스크에서는 동작하지 않던 경우가 있었던 것 같습니다(아니면 그 반대였을까요?).

아직 머릿속에 선명하실 때, 실제 전송 시에는 Discourse가 사용하는 것과 동일한 코드를 사용하는지 확인해 주시면 좋겠습니다.

우리는 또한 큐에 추가된 이메일 작업이 실패할 때 로깅을 개선하는 작업도 진행 중입니다. :+1:

운영 중인 포럼에서 제가 해야 할 일이 있나요?

아니요, 이 알림은 저희 호스팅에서 표시되어서는 안 됩니다. 수정하겠습니다. 항상 신고해 주셔서 감사합니다.

@supermathie 이 변수가 설정된 모든 사이트의 모든 관리자에게 개인 메시지를 보내는 것이 가치가 있을까요? 현재 문제 검사 시스템은 그렇게 하지만, 대부분의 경우 이것이 nil 효과를 가지므로 여기서는 그렇게 할 필요가 없다고 생각합니다. 이상적으로는 관리자 사용자에게 알림을 보내지 않고 대시보드에만 표시되길 원하는데, 현재 문제 검사 구조가 이 사용 사례를 지원하는지 확실하지 않습니다.

그렇게 생각해요. 이메일 설정에 대해 많은 관리자가 혼란을 겪고 있는 점을 고려하면, 실제로 STARTTLS에 의존하고 있음에도 불구하고 이 변수가 설정되어 있을 가능성이 매우 높다고 봅니다.

아마 아무도 이 변수를 설정해 두어서는 안 될 거예요.

누군가의 이메일 설정이 조용히 깨지는 것보다는 불필요한 경고를 보여주는 쪽이 훨씬 낫습니다.

대안은 이 체크를 제거하고, 변수를 아무것도 하지 않도록 비활성화하는 것입니다.

출력용 SMTP 서버가 localhost(즉, Discourse 도메인 이름과 일치하는 경우)인 경우, Docker 컨테이너와 호스트는 동일한 머신에 있으므로 TLS가 필요 없으므로 경고가 표시되지 않는 것이 좋겠습니다.

이 경우 환경에서 해당 변수를 제거해야 합니다.

Discourse는 STARTTLS가 제공되는 경우에만 이를 사용합니다.

현재 버전에서 주석 처리된 줄(기본값은 true)이 force_tls와 함께 이메일 전송을 차단하는 문제를 발견했습니다. 그래서 해당 줄을 주석 해제하고 false로 설정했습니다. 이제 이메일 전송은 정상적으로 작동하지만, 백엔드에서 이 메시지가 표시됩니다…

이메일 제공업체의 포트에 따라 상황이 조금씩 달라집니다… 두 가지 모두 작동하지 않으며, 확실히 영향을 미칩니다.

저도 이 문제를 발견했습니다. 11월 28일에 Discourse를 업데이트한 후, Job exception: :enable_starttls and :tls are mutually exclusive. Set :tls if you're on an SMTPS connection. Set :enable_starttls if you're on an SMTP connection and using STARTTLS for secure TLS upgrade.라는 오류 메시지와 함께 이메일 전송 실패가 발생하기 시작했습니다.

백트레이스
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mail-2.9.0/lib/mail/network/delivery_methods/smtp.rb:159:in `build_smtp_session'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mail-2.9.0/lib/mail/network/delivery_methods/smtp.rb:154:in `start_smtp_session'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mail-2.9.0/lib/mail/network/delivery_methods/smtp.rb:108:in `deliver!'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/mail-2.9.0/lib/mail/message.rb:269:in `deliver!'
/usr/local/lib/ruby/3.3.0/delegate.rb:87:in `method_missing'
/var/www/discourse/lib/email/sender.rb:296:in `send'
/var/www/discourse/app/jobs/regular/user_email.rb:80:in `send_user_email'
/var/www/discourse/app/jobs/regular/user_email.rb:40:in `execute'
/var/www/discourse/app/jobs/base.rb:318:in `block (2 levels) in perform'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rails_multisite-7.0.0/lib/rails_multisite/connection_management/null_instance.rb:49:in `with_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rails_multisite-7.0.0/lib/rails_multisite/connection_management.rb:17:in `with_connection'
/var/www/discourse/app/jobs/base.rb:305:in `block in perform'
/var/www/discourse/app/jobs/base.rb:301:in `each'
/var/www/discourse/app/jobs/base.rb:301:in `perform'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:220:in `execute_job'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:185:in `block (4 levels) in process'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:180:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'
/var/www/discourse/lib/sidekiq/suppress_user_email_errors.rb:6:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:182:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'
/var/www/discourse/lib/sidekiq/discourse_event.rb:6:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:182:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'
/var/www/discourse/lib/sidekiq/pausable.rb:131:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:182:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/job/interrupt_handler.rb:9:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:182:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/metrics/tracking.rb:26:in `track'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/metrics/tracking.rb:134:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:182:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:173:in `invoke'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:184:in `block (3 levels) in process'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:145:in `block (6 levels) in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/job_retry.rb:118:in `local'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:144:in `block (5 levels) in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/config.rb:39:in `block in <class:Config>'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:139:in `block (4 levels) in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:281:in `stats'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:134:in `block (3 levels) in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/job_logger.rb:15:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:133:in `block (2 levels) in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/job_retry.rb:85:in `global'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:132:in `block in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/job_logger.rb:40:in `prepare'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:131:in `dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:183:in `block (2 levels) in process'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:182:in `handle_interrupt'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:182:in `block in process'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:181:in `handle_interrupt'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:181:in `process'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:86:in `process_one'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:76:in `run'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/component.rb:10:in `watchdog'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/component.rb:19:in `block in safe_thread'

요한 크리스토프(Johann Christoph)와 마찬가지로, 저는 이전에 DISCOURSE_SMTP_ENABLE_START_TLS를 주석 처리하고 암시적 TLS(implicit TLS)를 위해 DISCOURSE_SMTP_FORCE_TLS를 true로 설정해 두었으며, 그때는 문제없이 잘 작동했습니다. 하지만 지금은 이메일 전송이 정상적으로 작동하려면 DISCOURSE_SMTP_ENABLE_START_TLS를 명시적으로 false로 설정해야 하며, 당연히 이는 관리자 대시보드에서 aforementioned 메시지가 트리거되게 만듭니다.

force_tls가 이메일 전송을 차단하는 원인이 될 것입니다. 이 설정은 연결 시점에 TLS를 강제하므로, 아마도 포트 465에만 적합한 설정일 것입니다.

정말 놀라운 일이네요!

사용 중인 SMTP 파라미터는 무엇인가요? (사용자 이름/비밀번호 제외)

포트 465, ENABLE_START_TLS=false, FORCE_TLS=true, OPEN_TIMEOUT=10. 마지막 설정은 약 3년 전 시스템 업데이트 이후부터 이상한 타임아웃 오류가 발생하기 시작해서 그랬는데, 위에서 설명한 문제와 관련이 있다면 모자를 먹고 살겠습니다.

그동안 커밋 히스토리를 살펴보다가, 제 환경에서 문제가 발생한 업데이트 바로 3일 전에 mail 게미가 2.9.0 버전으로 업데이트되었음을 확인했습니다. 해당 릴리스 노트에는 "SMTP: refactor and accept starttls :always and :auto by @eval in #1536"라는 항목이 있습니다. 저는 Ruby에 대해 많이 알지 못하지만, 참조된 PR의 이 변경 사항은 다소 의심스럽습니다:

DISCOURSE_SMTP_ENABLE_START_TLS가 기본적으로 true(즉, 주석 처리된 경우)로 문서화되어 있는 점을 고려할 때, 이것이 문제의 근본 원인일 수 있을까요?

앞서 말했듯이, force_TLS를 설정했습니다:

`DISCOURSE_SMTP_FORCE_TLS = true`

그리고 ENABLE_START_TLS를 명시적으로 false로 설정했습니다… 그러면 메일 발송이 정상적으로 작동합니다 - 단, 메일 서버가 465 포트에서 실행되는 경우에만요.

567 포트에서 실행되는 메일 서버의 경우 - 설정은 정확히 반대가 되어야 합니다.

그 이후로는 백엔드 대시보드에 경고 메시지가 표시되긴 하지만, 메일 발송은 문제없이 작동하고 있습니다.

DISCOURSE_SMTP_ENABLE_START_TLS를 false로 설정하려면 주석 처리해야 합니다. 그렇지 않으면 기본적으로 true로 설정되어 있기 때문입니다. 바로 이것이 제 문제를 일으킨 원인이었습니다. 다만, 최신 빌드(2025.12.0-latest)에서야 비로소 해당 문제가 발생했습니다.

와, 이건 정말 엉성한 변경이네요.

enable_starttls은 starttls 사용을 강제하지만, enable_starttls_auto는 기회주의적입니다 - 서버가 제공하면 TLS를 협상할 뿐입니다.

그리고 메일 서버가 초기 TLS로 연결된 경우, starttls를 제공하지 않습니다:

○ → openssl s_client -connect localhost:5587 -starttls smtp
250 CHUNKING
EHLO localhost
250-testmailrelay
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-AUTH PLAIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250 CHUNKING

도대체 왜 그렇게 했을까요? :facepalm:

여기서 어려운 점은 처음부터 이 구성을 제공했어야 한다는 것입니다. 다음과 같은 형태였어야 합니다:

DISCOURSE_SMTP_TLS_MODE = starttls_auto # [ none | starttls | starttls_auto (기본값) | tls ]

사람들이 설정을 더 많이 하는 대신, 더 적게 하도록 하려는 취지에서, 이 방식이 가장 좋다고 생각합니다:

이제 병합되었습니다. 더 이상 STARTTLS를 명시적으로 비활성화할 필요가 없으며, TLS가 활성화되어 있으면 Discourse가 이를 자동으로 처리합니다.

네, 다시 정상적으로 작동합니다. 감사합니다!

좋아요. STARTTLS를 비활성화할 필요가 없다는 경고가 나와서 짜증이 났는데, 사실 저희 SMTP 서버는 STARTTLS를 아예 지원하지 않거든요(암시적 TLS만 지원). 그래서 DISCOURSE_SMTP_FORCE_TLS=true를 설정하면 STARTTLS가 암시적으로 비활성화되는 건가요?